Скругление углов View в iOS: от базового радиуса до кастомных фигур

Скруглённые углы элементов интерфейса стали неотъемлемой частью дизайна iOS-приложений, начиная с первых версий системы. От простых кнопок с cornerRadius до сложных кастомных фигур с CAShapeLayer — возможности платформы позволяют реализовать практически любой дизайнерский замысел. Однако даже опытные разработчики иногда сталкиваются с неожиданными артефактами: обрезанными тенями, неправильным отображением при анимации или падением производительности на старых устройствах.

Эта статья покрывает все актуальные способы скругления углов в iOS 15-17, включая новые возможности SwiftUI и оптимизированные подходы для UIKit. Мы разберём не только базовые методы вроде layer.cornerRadius, но и продвинутые техники с использованием масок, UIBezierPath, и даже динамическое скругление с учётом safeAreaInsets. Особое внимание уделено производительности — вы узнаете, как избежать дорогостоящего рендеринга offscreen и почему иногда лучше использовать CALayer.maskedCorners вместо стандартного радиуса.

1. Базовое скругление с помощью cornerRadius

Самый простой и распространённый способ — использование свойства cornerRadius у слоя UIView. Этот метод работает во всех версиях iOS и подходит для большинства стандартных задач:

```swift

myView.layer.cornerRadius = 12

myView.layer.masksToBounds = true // Обрезает содержимое по границам

```

Важно понимать, что masksToBounds = true не только обрезает содержимое, но и отключает тени у вьюхи. Если вам нужны одновременно и скруглённые углы, и тень — придётся использовать другой подход (разберём его в разделе про CAShapeLayer).

  • ✅ Простота реализации (1 строка кода)
  • ✅ Поддержка во всех версиях iOS
  • ⚠️ Проблемы с производительностью при анимации
  • ⚠️ Не работает с тенями при masksToBounds = true
⚠️ Внимание: На устройствах с чипом A8 (iPhone 6/6 Plus) и старше использование cornerRadius на вьюхах с большим количеством субвью может вызывать лаги при скролле. В таких случаях лучше использовать UIBezierPath с маской.
📊 Какой способ скругления углов вы используете чаще?
cornerRadius
CAShapeLayer
UIBezierPath с маской
SwiftUI .clipShape
Другой

2. Скругление отдельных углов (CALayer.maskedCorners)

Начиная с iOS 11, появилась возможность скруглять только выбранные углы с помощью свойства maskedCorners. Это особенно полезно для кастомных ячеек таблиц или коллекций, где нужно скруглить только верхние или нижние углы:

```swift

myView.layer.cornerRadius = 12

myView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] // Только верхние углы

```

Доступные опции для CALayer.CACornerMask:

ОпцияОписаниеВизуализация
.layerMinXMinYCornerЛевый верхний угол⬛◻️◻️
◻️◻️◻️
.layerMaxXMinYCornerПравый верхний угол◻️◻️⬛
◻️◻️◻️
.layerMinXMaxYCornerЛевый нижний угол◻️◻️◻️
⬛◻️◻️
.layerMaxXMaxYCornerПравый нижний угол◻️◻️◻️
◻️◻️⬛

Для совместимости с iOS 10 и ниже можно использовать альтернативный подход с UIRectCorner и UIBezierPath:

```swift

let maskPath = UIBezierPath(

roundedRect: myView.bounds,

byRoundingCorners: [.topLeft, .topRight],

cornerRadii: CGSize(width: 12, height: 12)

)

let maskLayer = CAShapeLayer()

maskLayer.path = maskPath.cgPath

myView.layer.mask = maskLayer

```

3. Продвинутое скругление с CAShapeLayer

Когда требуется не только скруглить углы, но и добавить тень или градиент — на помощь приходит CAShapeLayer. Этот метод более производительный, чем стандартный cornerRadius, особенно для сложных фигур:

```swift

let shapeLayer = CAShapeLayer()

shapeLayer.path = UIBezierPath(

roundedRect: myView.bounds,

cornerRadius: 16

).cgPath

shapeLayer.fillColor = UIColor.systemBlue.cgColor

shapeLayer.shadowColor = UIColor.black.cgColor

shapeLayer.shadowOffset = CGSize(width: 0, height: 2)

shapeLayer.shadowOpacity = 0.3

shapeLayer.shadowRadius = 4

// Добавляем как сублейер (не маску!)

myView.layer.insertSublayer(shapeLayer, at: 0)

```

Ключевые преимущества этого подхода:

  • 🎨 Поддержка теней и градиентов одновременно со скруглёнными углами
  • ⚡ Лучшая производительность при анимации (рендеринг происходит на GPU)
  • 🔧 Гибкость: можно создать любую форму, не только прямоугольник
⚠️ Внимание: При использовании CAShapeLayer для кнопок (UIButton) не забудьте отключить стандартное затенение с помощью button.layer.shouldRasterize = false, иначе тень будет рендериться дважды.
Как оптимизировать CAShapeLayer для таблиц с сотнями ячеек?

Для оптимизации производительности в UITableView или UICollectionView:

1. Создайте один экземпляр CAShapeLayer и переиспользуйте его через prepareForReuse

2. Используйте UIBezierPath с фиксированными размерами (например, для стандартной высоты ячейки)

3. Отключите неявные анимации с помощью CATransaction.setDisableActions(true) при обновлении пути

4. Скругление в SwiftUI: clipShape и cornerRadius

В SwiftUI скругление углов реализуется проще, чем в UIKit, благодаря модификаторам .cornerRadius() и .clipShape(). Базовый пример:

```swift

Text("Hello, World!")

.padding()

.background(Color.blue)

.cornerRadius(10) // Простое скругление

```

Для более сложных форм используйте clipShape с RoundedRectangle или кастомными фигурами:

```swift

RoundedRectangle(cornerRadius: 25)

.fill(Color.green)

.frame(width: 200, height: 100)

.overlay(

Text("Custom Shape")

.foregroundColor(.white)

)

```

Важный нюанс: В SwiftUI модификатор .cornerRadius автоматически обрезает содержимое (аналог masksToBounds = true в UIKit), но при этом сохраняет возможность добавления теней через .shadow().

  • 🔄 Для динамического изменения радиуса используйте @State или @Binding
  • 🎨 Для скругления только части углов создайте кастомную Shape с UIBezierPath
  • ⚡ В iOS 17 появился новый модификатор .clipShape(/@START_MENU_TOKEN@/Circle()/@END_MENU_TOKEN@/, style: FillStyle()) с поддержкой eoFill и nonZero правил заполнения

5. Скругление с учётом safeArea и динамических размеров

При работе с safeAreaInsets (например, в iPhone X и новее) стандартное скругление может приводить к визуальным артефактам. Чтобы корректно обработать такие случаи, нужно:

  1. Получить текущие safeAreaInsets через UIApplication.shared.windows.first?.safeAreaInsets
  2. Скорректировать bounds вьюхи с учётом инсетов
  3. Применить скругление к модифицированному прямоугольнику

Пример для UIKit:

```swift

override func layoutSubviews() {

super.layoutSubviews()

let safeInsets = UIApplication.shared.windows.first?.safeAreaInsets ?? .zero

let adjustedRect = bounds.inset(by: UIEdgeInsets(

top: safeInsets.top,

left: 0,

bottom: safeInsets.bottom,

right: 0

))

let maskPath = UIBezierPath(

roundedRect: adjustedRect,

cornerRadius: 16

)

let maskLayer = CAShapeLayer()

maskLayer.path = maskPath.cgPath

layer.mask = maskLayer

}

```

В SwiftUI аналогичный эффект достигается с помощью модификатора .ignoresSafeArea() в комбинации с .clipShape():

```swift

ZStack {

RoundedRectangle(cornerRadius: 16)

.fill(Color.white)

.ignoresSafeArea(edges: .bottom) // Учитываем safeArea

}

```

Убедитесь что:

✓ Радиус скругления не превышает минимальную высоту вьюхи с учётом инсетов

✓ Для UITableView скругление применено в layoutSubviews, а не в awakeFromNib

✓ При использовании UIScrollView отключены delaysContentTouches = false и canCancelContentTouches = true

-->

6. Производительность: как избежать лагов при скруглении

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

ПроблемаРешениеПрирост FPS
Дорогостоящий рендеринг offscreenИспользовать CAShapeLayer вместо cornerRadius+15-20%
Лаги при скролле UITableViewКэшировать маски в prepareForReuse+25%
Артефакты при анимацииУстановить layer.shouldRasterize = true и layer.rasterizationScale = UIScreen.main.scale+10%
Замедление при большом количестве вьюхОбъединять вьюхи с одинаковым скруглением в один контейнер+30%

Для диагностики проблем с производительностью используйте инструмент Time Profiler в Xcode:

  1. Запустите профилирование (⌘ + I)
  2. Выберите Time Profiler
  3. Прокрутите проблемный экран и найдите пики CPU
  4. Ищите вызовы CALayerRender или UIKitCore`-[UIView(Geometry) _applyAnimatedBounds

7. Кастомные формы: от круга до «дырок» в вьюхе

Иногда требуется не просто скруглить углы, а создать полностью кастомную форму — например, круг, овал или даже вьюху с «дыркой» посередине. Для этого используйте комбинацию UIBezierPath и CAShapeLayer:

Пример 1: Идеальный круг

```swift

let circlePath = UIBezierPath(ovalIn: myView.bounds)

let maskLayer = CAShapeLayer()

maskLayer.path = circlePath.cgPath

myView.layer.mask = maskLayer

```

Пример 2: Вьюха с круглым вырезом

```swift

let path = UIBezierPath(rect: myView.bounds)

let circlePath = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 100, height: 100))

path.append(circlePath)

path.usesEvenOddFillRule = true // Правило "дырки"

let maskLayer = CAShapeLayer()

maskLayer.path = path.cgPath

maskLayer.fillRule = .evenOdd

myView.layer.mask = maskLayer

```

В SwiftUI аналогичный эффект достигается с помощью модификатора .mask():

```swift

Circle()

.frame(width: 200, height: 200)

.mask(

Rectangle()

.frame(width: 200, height: 200)

.overlay(

Circle()

.frame(width: 100, height: 100)

.blendMode(.destinationOut)

)

)

```

FAQ: Частые вопросы по скруглению углов в iOS

Почему тень обрезается при использовании cornerRadius?

Это происходит потому, что masksToBounds = true обрезает всё содержимое вьюхи, включая тень. Решения:

  1. Используйте CAShapeLayer для тени (как показано в разделе 3)
  2. Поместите вьюху в контейнер и примените тень к контейнеру, а скругление — к дочерней вьюхе
  3. В SwiftUI используйте .shadow() после .clipShape()
Как сделать скругление только для верхних углов ячейки UITableView?

Для первой/последней ячейки в секции:


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {

let cornerRadius: CGFloat = 12

var corners: UIRectCorner = []

if indexPath.row == 0 {

corners.update(with: [.topLeft, .topRight])

} else if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {

corners.update(with: [.bottomLeft, .bottomRight])

}

let maskPath = UIBezierPath(

roundedRect: cell.bounds,

byRoundingCorners: corners,

cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)

)

let maskLayer = CAShapeLayer()

maskLayer.path = maskPath.cgPath

cell.layer.mask = maskLayer

}

Не забудьте сбросить маску в prepareForReuse!

Почему анимация cornerRadius дергается?

Это происходит из-за того, что CALayer по умолчанию использует неявные анимации для cornerRadius, которые конфликтуют с вашими явными анимациями. Решения:

  • Отключите неявные анимации: CATransaction.setDisableActions(true)
  • Используйте CABasicAnimation для плавного изменения радиуса
  • Для сложных анимаций переходите на CAShapeLayer
Как сделать скругление с градиентной обводкой?

Создайте два слоя: один для фона, другой для обводки:


let backgroundLayer = CAShapeLayer()

backgroundLayer.path = UIBezierPath(roundedRect: myView.bounds, cornerRadius: 16).cgPath

backgroundLayer.fillColor = UIColor.systemBlue.cgColor

let borderLayer = CAShapeLayer()

borderLayer.path = backgroundLayer.path

borderLayer.lineWidth = 2

borderLayer.strokeColor = UIColor.systemPurple.cgColor

borderLayer.fillColor = UIColor.clear.cgColor

// Добавляем градиент к обводке

let gradient = CAGradientLayer()

gradient.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]

gradient.startPoint = CGPoint(x: 0, y: 0.5)

gradient.endPoint = CGPoint(x: 1, y: 0.5)

gradient.frame = myView.bounds

gradient.mask = borderLayer

myView.layer.addSublayer(backgroundLayer)

myView.layer.addSublayer(gradient)

Поддерживает ли SwiftUI скругление только части углов?

В стандартной библиотеке нет прямого аналога maskedCorners, но можно создать кастомную форму:


struct TopRoundedRectangle: Shape {

var cornerRadius: CGFloat

func path(in rect: CGRect) -> Path {

var path = Path()

let width = rect.size.width

let height = rect.size.height

path.move(to: CGPoint(x: 0, y: cornerRadius))

path.addArc(

center: CGPoint(x: cornerRadius, y: cornerRadius),

radius: cornerRadius,

startAngle: Angle(degrees: 180),

endAngle: Angle(degrees: 270),

clockwise: false

)

path.addLine(to: CGPoint(x: width - cornerRadius, y: 0))

path.addArc(

center: CGPoint(x: width - cornerRadius, y: cornerRadius),

radius: cornerRadius,

startAngle: Angle(degrees: 270),

endAngle: Angle(degrees: 0),

clockwise: false

)

path.addLine(to: CGPoint(x: width, y: height))

path.addLine(to: CGPoint(x: 0, y: height))

path.closeSubpath()

return path

}

}

// Использование:

TopRoundedRectangle(cornerRadius: 12)

.fill(Color.blue)

.frame(width: 200, height: 100)