Скруглённые углы элементов интерфейса стали неотъемлемой частью дизайна 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с маской.
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 и новее) стандартное скругление может приводить к визуальным артефактам. Чтобы корректно обработать такие случаи, нужно:
- Получить текущие
safeAreaInsetsчерезUIApplication.shared.windows.first?.safeAreaInsets - Скорректировать
boundsвьюхи с учётом инсетов - Применить скругление к модифицированному прямоугольнику
Пример для 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:
- Запустите профилирование (
⌘ + I) - Выберите
Time Profiler - Прокрутите проблемный экран и найдите пики CPU
- Ищите вызовы
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 обрезает всё содержимое вьюхи, включая тень. Решения:
- Используйте
CAShapeLayerдля тени (как показано в разделе 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)