Avatar ·

Как скруглить только определенные углы View в SwiftUI?

Всем привет! Я новичок в SwiftUI и столкнулся с проблемой: мне нужно скруглить только два верхних угла у прямоугольного View, а нижние оставить прямыми. В обычном UIKit я использовал UIBezierPath и CAShapeLayer, но в SwiftUI, похоже, нет прямого модификатора для выборочного скругления.

Я пробовал использовать .cornerRadius(20), но он скругляет все четыре угла. Также я читал про .clipShape(RoundedRectangle(cornerRadius: 20)), но это тоже не решает задачу — скругляются все углы.

Вот пример моего кода:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Привет, мир!")
                .padding()
                .background(Color.blue)
                .cornerRadius(20) // скругляет все углы
        }
        .padding()
    }
}

Мне нужно, чтобы верхние углы были закруглены, а нижние оставались прямыми. Как это сделать в SwiftUI? Есть ли встроенный модификатор, или придётся использовать кастомные решения? Буду благодарен за примеры кода!

<<>>

Использование кастомной формы с помощью модификатора .clipShape

В SwiftUI нет встроенного модификатора для выборочного скругления углов, но это легко решается созданием кастомной формы, которая наследует протокол Shape. Мы можем нарисовать путь с помощью UIBezierPath или встроенного Path из SwiftUI и указать, какие углы скруглять, а какие — нет.

Вот готовое решение:

struct RoundedCorners: Shape {
    var topLeft: CGFloat = 0
    var topRight: CGFloat = 0
    var bottomLeft: CGFloat = 0
    var bottomRight: CGFloat = 0

    func path(in rect: CGRect) -> Path {
        let minX = rect.minX
        let minY = rect.minY
        let maxX = rect.maxX
        let maxY = rect.maxY

        var path = Path()

        // Начинаем с левого верхнего угла
        path.move(to: CGPoint(x: minX + topLeft, y: minY))

        // Верхняя сторона
        path.addLine(to: CGPoint(x: maxX - topRight, y: minY))
        // Правый верхний угол
        path.addArc(center: CGPoint(x: maxX - topRight, y: minY + topRight),
                    radius: topRight,
                    startAngle: Angle(degrees: -90),
                    endAngle: Angle(degrees: 0),
                    clockwise: false)

        // Правая сторона
        path.addLine(to: CGPoint(x: maxX, y: maxY - bottomRight))
        // Правый нижний угол
        path.addArc(center: CGPoint(x: maxX - bottomRight, y: maxY - bottomRight),
                    radius: bottomRight,
                    startAngle: Angle(degrees: 0),
                    endAngle: Angle(degrees: 90),
                    clockwise: false)

        // Нижняя сторона
        path.addLine(to: CGPoint(x: minX + bottomLeft, y: maxY))
        // Левый нижний угол
        path.addArc(center: CGPoint(x: minX + bottomLeft, y: maxY - bottomLeft),
                    radius: bottomLeft,
                    startAngle: Angle(degrees: 90),
                    endAngle: Angle(degrees: 180),
                    clockwise: false)

        // Левая сторона
        path.addLine(to: CGPoint(x: minX, y: minY + topLeft))
        // Левый верхний угол
        path.addArc(center: CGPoint(x: minX + topLeft, y: minY + topLeft),
                    radius: topLeft,
                    startAngle: Angle(degrees: 180),
                    endAngle: Angle(degrees: 270),
                    clockwise: false)

        return path
    }
}

Теперь вы можете использовать эту форму с модификатором .clipShape:

struct ContentView: View {
    var body: some View {
        Text("Привет, мир!")
            .padding()
            .background(Color.blue)
            .clipShape(RoundedCorners(topLeft: 20, topRight: 20, bottomLeft: 0, bottomRight: 0))
    }
}

Как это работает:

  • Мы создаём структуру RoundedCorners, которая принимает радиусы для каждого угла (по умолчанию 0).
  • В методе path(in:) мы рисуем контур прямоугольника, используя addArc для скругления углов, где радиус больше 0.
  • Для углов с радиусом 0 мы просто рисуем прямые линии.
  • Модификатор .clipShape обрезает view по заданной форме.

Дополнительные советы:

  • Если вам нужно скруглить только два верхних угла, как в вашем примере, передайте радиусы только для них: topLeft: 20, topRight: 20.
  • Вы можете комбинировать это с другими модификаторами, например, с .overlay для добавления границы с такими же скруглениями.
  • Для более сложных случаев (например, разные радиусы для каждого угла) эта форма легко расширяется.

Это решение полностью кастомизируемо и работает во всех версиях iOS, начиная с iOS 13. Оно также совместимо с macOS, watchOS и tvOS.

<<>>

Альтернативное решение: использование модификатора .cornerRadius с наложением

Если вам не хочется создавать кастомную форму, можно использовать простой трюк: наложить один view на другой, чтобы скрыть скруглённые углы. Например, чтобы оставить скруглёнными только верхние углы, можно сделать так:

struct ContentView: View {
    var body: some View {
        VStack(spacing: 0) {
            // Верхняя часть со скруглёнными углами
            Text("Привет, мир!")
                .padding()
                .frame(maxWidth: .infinity)
                .background(Color.blue)
                .cornerRadius(20)

            // Нижняя часть без скругления
            Rectangle()
                .fill(Color.blue)
                .frame(height: 10) // маленькая полоска, чтобы перекрыть нижние углы
        }
        .frame(width: 200) // задайте нужную ширину
    }
}

Как это работает:

  • Мы используем VStack с нулевым отступом между элементами (spacing: 0).
  • Верхний view скругляет все углы, но нижний view (прямоугольник) перекрывает нижние скруглённые углы, делая их прямыми.
  • Высота нижнего прямоугольника должна быть достаточной, чтобы перекрыть радиус скругления (в примере 10 пикселей — это больше, чем радиус 20? Нет, здесь нужно подобрать высоту, но лучше использовать cornerRadius с запасом).

Недостатки этого подхода:

  • Он менее гибкий: если фон не сплошной (например, градиент или изображение), наложение будет заметно.
  • Требуется точная настройка размеров, что неудобно при динамическом контенте.
  • Не работает с прозрачными фонами.

Этот метод подходит только для простых случаев с однотонным фоном. Для продакшена я рекомендую использовать первый подход с кастомной формой — он надёжнее и красивее.