Вопрос: Стой, что еще за “дырка” такая?

Ответ: Условимся, что дыркой будем называть часть UIView, которая будет вырезана для того, чтобы можно было смотреть как-бы “cквозь” UIView

Рис 1. Дырка

На рисунке 1 представлена иерархия дырки. Cлева направо:

  • несколько системных окон UIWindowScene;
  • gradientView, градиентная UIView;
  • holeView, прозрачная UIView, которая будет задавать размер дырки;
  • whiteView, белая UIView с дыркой.

Вопрос: Как такое сделать?

Ответ: засунуть в viewDidLoad() функцию makeHole():

func makeHole() {// Берем путь у whiteView    let path = UIBezierPath(rect: whiteView.bounds)// Создаем путь у holeView и скругляем его    let pathWithRadius = UIBezierPath(
roundedRect: holeView.frame,
byRoundingCorners: [.allCorners],
cornerRadii: holeView.frame.size)
// Создаем общую геометрию пути path.append(pathWithRadius)// Создаем маску этой геометрии и применяем правило заливки evenOdd, которое как раз и делает дырку let mask = CAShapeLayer()
mask.path = path.cgPath
mask.fillRule = CAShapeLayerFillRule.evenOdd
// Применяем полученную маску к whiteView whiteView.layer.mask = mask}

Для тех, кто плохо воспринимает код по снипетам, сразу прикладываю исходный код проекта

Теперь для создания дырявой UITableView надо создать HoleTableViewCell, которая будет повторять иерархию с первого рисунка и в реализации содержать аналогичную функцию makeHole(). Вызывать ее будем в методе делегата tableView(_:willDisplay:forRowAt:):

func tableView(_ tableView: UITableView, 
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath) {
if let holeCell = cell as? HoleTableViewCell {

holeCell.layoutIfNeeded()
holeCell.makeHole()
}}
Рис 2. Дырявый UITableView

Отлично! Но по задумке дизайнера нужно двигаться дальше, я посмотрел в фигму и увидел следующий паттерн:

Рис 3. Паттерн выреза

С помошью extension к UIImageView реализуется логическое вычетание XOR. Оно делает дырку в image по заданному паттерну:

extension UIImageView {    func applyGradientPattern(_ pattern: UIImage) {      let size = frame.size
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)
UIGraphicsBeginImageContext(size)
self.image!.draw(in: rect)
pattern.draw(in: rect, blendMode: .destinationOut, alpha: 1.0)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.image = newImage
}}

И вызвав эту функцию в методе делегата tableView(_:willDisplay:forRowAt:):

func tableView(_ tableView: UITableView, 
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath) {
if let gradientHoleCell = cell as? GradientHoleTableViewCell {

gradientHoleCell.layoutIfNeeded()
gradientHoleCell.applyGradientPattern()
gradientHoleCell.makeHole()
}}

Получил следующий результат:

Рис 4. Градиентно дырявый UITableView

Вопрос: Что произошло простыми словами?

Ответ: Мы сделали дырку в whiteView размером с holeImageView, а потом дырку в holeImageView по полученному от дизайнера паттерну

В процессе работы над спринтом естественно всплыло несколько косяков.

Например, если общая сумма высот ячеек меньше высоты экрана без safeArea, то происходило следующее:

Рис 5. Маленькое количество ячеек

Проблема решалась подсчетом высоты контента и

tableView.bounces = false

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

Надеюсь было интересно!)

Тут можно глянуть код проекта

И я открыт к любым вопросам по этой теме

P.S. У нас в mileonair, в разделе акций, можно увидеть дырку идущую сквозь UIImageView, UITableView и UICollectionView одновременно

iOS dev in mileonair

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store