Как сделать адаптивный tutorial для iOS приложения без дизайнера

При разработке приложения иногда нужно добавить tutorial, чтобы пользователь быстро разобрался как использовать ваше приложение. В данной статье я расскажу как сделать простой и адаптивный tutorial для iOS приложения. За пример возьму небольшое приложение с контактами.

Screenshot 1
Screenshot 1

Цель tutorial показать функционал приложения. На этом экране это добавление нового контакта, просмотр контакта и переключение между списком контактов и настройками приложения.

Предположим что нам надо сделать вот такой tutorial:

Screenshot 2
Screenshot 2

Для этого нужно:

- картинка черного цвета с alpha равной 0.9

- сделать прозрачные прямоугольники под нужные элементы UI

- картинки стрелок

- текст к стрелкам

- добавить необходимые constraints для layout

Нарисуем картинку черного цвета и выставим alpha для UIImageView равной 0.9 :

func drawBlackImage() -> UIImage { let size = UIScreen.main.bounds.size let renderer = UIGraphicsImageRenderer(size: size) let image = renderer.image { context in UIColor.black.setFill() context.fill(CGRect(origin: .zero, size: size)) } return image }
Screenshot 3
Screenshot 3

Для того чтобы вырезать из картинки прямоугольники воспользуемся возможностями Core Image. Применим к картинке фильтр CIMaskToAlpha, но для этого нам сначала нужно подготовить нашу картинку. Фильтр CIMaskToAlpha черные пиксели картинки делает прозрачными, а белые пиксели оставляет без изменений. Поэтому нам нужно сделать картинку белого цвета и добавить на нее необходимые прямоугольники черного цвета.

Картинка с белым фоном и черными прямоугольниками:

// функция drawCroppingRectangles принимает один параметр [CGRect] // и рисует по этим размерам и расположениям черные прямоугольники // на белом фоне func drawCroppingRectangles(to rects: [CGRect]) -> UIImage { let size = UIScreen.main.bounds.size let renderer = UIGraphicsImageRenderer(size: size) let image = renderer.image { context in // рисуем белый фон UIColor.white.setFill() context.fill(CGRect(origin: .zero, size: size)) // рисуем черные прямоугольники по входным данным rects.forEach { rect in var cRect = rect // добавим небольшие отступы от исходных размеров let dX: CGFloat = 3 let dY: CGFloat = 3 cRect.origin.x -= dX cRect.origin.y -= dY cRect.size.width += dX * 2 cRect.size.height += dY * 2 // добавим закругленные края у прямоугольников let path = UIBezierPath(roundedRect: cRect, cornerRadius: 6).cgPath context.cgContext.addPath(path) } context.cgContext.setFillColor(UIColor.black.cgColor) context.cgContext.closePath() context.cgContext.fillPath() } return image }

Применяем фильтр CIMaskToAlpha:

func cropBlackRectangles(image: UIImage) -> UIImage? { let context = CIContext() guard let filter = CIFilter(name: "CIMaskToAlpha") else { return nil } guard let cgImage = image.cgImage else { return nil } let ciImage = CIImage(cgImage: cgImage) filter.setDefaults() filter.setValue(ciImage, forKey: kCIInputImageKey) guard let outputImage = filter.outputImage else { return nil } guard let cgOutputImage = context.createCGImage(outputImage, from: outputImage.extent) else { return nil } return UIImage(cgImage: cgOutputImage) }

Перекрасим картинку в черный цвет с помощью фильтра CIFalseColor:

func changeColor(_ color: UIColor, newColor: UIColor, image: UIImage) -> UIImage? { guard let falseFilter = CIFilter(name: "CIFalseColor") else { return nil } guard let cgImage = image.cgImage else { return nil } let ciImage = CIImage(cgImage: cgImage) falseFilter.setValue(ciImage, forKey: kCIInputImageKey) falseFilter.setValue(CIColor(color: color), forKey: "inputColor0") falseFilter.setValue(CIColor(color: newColor), forKey: "inputColor1") guard let outputImage = falseFilter.outputImage else { return nil } return UIImage(ciImage: outputImage) }

Результат:

Screenshot 4
Screenshot 4

Теперь нам нужна функция для рисования стрелки. Рисовать будем с помощью Core Graphics.

Picture 1
Picture 1

Рассчитаем 7 координат, соединим их и закрасим:

extension UIBezierPath { class func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> Self { let length = hypot(end.x - start.x, end.y - start.y) let tailLength = length - headLength // расчет координат let points: [CGPoint] = [CGPoint(x: 0, y: tailWidth / 2), CGPoint(x: tailLength, y: tailWidth / 2), CGPoint(x: tailLength, y: headWidth / 2), CGPoint(x: length, y: 0), CGPoint(x: tailLength, y: -headWidth / 2), CGPoint(x: tailLength, y: -tailWidth / 2), CGPoint(x: 0, y: -tailWidth / 2) ] let cosine = (end.x - start.x) / length let sine = (end.y - start.y) / length // поворачиваем стрелку в зависимости от того как расположены начальная // и конечная координаты let transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y) let path = CGMutablePath() path.addLines(between: points, transform: transform) path.closeSubpath() return self.init(cgPath: path) } } // функция для рисования стрелки // size - размер картинки // color - цвет стрелки // startPoint - начальная координата стрелки // endPoint - конечная координата стрелки func drawArrow(size: CGSize, color: UIColor, startPoint: CGPoint, endPoint: CGPoint) -> UIImage { let renderer = UIGraphicsImageRenderer(size: size) let image = renderer.image { ctx in ctx.cgContext.setFillColor(color.cgColor) ctx.cgContext.setLineWidth(2) let bezierPath = UIBezierPath.arrow(from: startPoint, to: endPoint, tailWidth: 2, headWidth: 15, headLength: 15) ctx.cgContext.addPath(bezierPath.cgPath) ctx.cgContext.fillPath() } return image }

Еще одна картинка для понимания как считаются координаты стрелки:

Picture 2
Picture 2

Параметры для рисования стрелок в моем примере:

let size = CGSize(width: 50, height: 250) let startPoint = CGPoint(x: 0, y: size.height) let endPoint = CGPoint(x: size.width - 10, y: 0) let arrowImage = DrawHelper.drawArrow(size: size, color: .white, startPoint: startPoint, endPoint: endPoint)
let size = CGSize(width: 40, height: 100) let startPoint = CGPoint(x: size.width, y: size.height) let endPoint = CGPoint(x: size.width / 2, y: 0) let arrowImage = DrawHelper.drawArrow(size: size, color: .white, startPoint: startPoint, endPoint: endPoint)
let size = CGSize(width: 40, height: 50) let startPoint = CGPoint(x: size.width, y: size.height) let endPoint = CGPoint(x: size.width / 2, y: 0) let arrowImage = DrawHelper.drawArrow(size: size, color: .white, startPoint: startPoint, endPoint: endPoint)

Стрелки для элементов UITabBar имеют одни параметры, но разные constraints.

let size = CGSize(width: 20, height: 100) let startPoint = CGPoint(x: size.width / 2, y: 0) let endPoint = CGPoint(x: size.width / 2, y: size.height) let arrowImage = DrawHelper.drawArrow(size: size, color: .white, startPoint: startPoint, endPoint: endPoint)

Остается добавить constrains для UImageView, установить значение image для UIImageView и добавить UILabel с описанием.

Так как все делается по аналогии приведу пример только avatar.

Пример для аватара:

func configureAvatar(_ cell: UITableViewCell) { guard let userCell = cell as? UserCell else { return } // вычисляем позицию аватара пользователя относительно всего экрана // tutorialView "прибита" по краям экрана с учетом игнорирования safeArea let avatarRect = userCell.avatarContainerView.convert(userCell.avatarContainerView.bounds, to: self) // добавляем новый frame аватары в общий массив frames который мы потом отдаем // на "вырезание" прямоугольников rects.append(avatarRect) // рисование и добавление стрелки на tutorialView let size = CGSize(width: 40, height: 100) // создаем UIImageView с constraints width и height по size let arrowAvatarImageView = UIImageView.imageView(with: size) let startPoint = CGPoint(x: size.width, y: size.height) let endPoint = CGPoint(x: size.width / 2, y: 0) let arrowAvatarImage = DrawHelper.drawArrow(size: size, color: .white, startPoint: startPoint, endPoint: endPoint) arrowAvatarImageView.image = arrowAvatarImage addSubview(arrowAvatarImageView) // задаем constraints для UIImageView NSLayoutConstraint.activate([arrowAvatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: avatarRect.maxY + 10), arrowAvatarImageView.centerXAnchor.constraint(equalTo: userCell.avatarContainerView.centerXAnchor) ]) // создаем UILabel с настройками шрифта, цвета текста и другими настройками let avatarDescriptionLabel = UILabel.labelForTutorial(text: "Аватар контакта") addSubview(avatarDescriptionLabel) // задаем constraints для UILabel NSLayoutConstraint.activate([avatarDescriptionLabel.widthAnchor.constraint(equalToConstant: descriptionWidth), avatarDescriptionLabel.topAnchor.constraint(equalTo: arrowAvatarImageView.bottomAnchor, constant: 10), avatarDescriptionLabel.trailingAnchor.constraint(equalTo: arrowAvatarImageView.trailingAnchor, constant: descriptionWidth / 2) ]) }

На скриншотах можно посмотреть что layout не поехал на разных девайсах с разным разрешением.

Screenshot 5. iPhone 8 Plus
Screenshot 5. iPhone 8 Plus
Screenshot 6. iPhone 11
Screenshot 6. iPhone 11
Screenshot 7. iPhone 11 Pro Max
Screenshot 7. iPhone 11 Pro Max
Screenshot 8. iPhone SE 2020
Screenshot 8. iPhone SE 2020

Это простой пример что можно сделать с помощью Core Image и Core Graphics.

Код тестового проекта можно посмотреть тут:

Спасибо за внимание.

#selectel_инструкция

11
Начать дискуссию