Image-Driven Development: как минимизировать время разработчиков при реализации MVP-функционала
Привет, меня зовут Сергей Матвеев. Я iOS Lead в компании OBRIO, которая входит в экосистему бизнесов Genesis и занимается разработкой мобильных приложений и игр. В этой статье я расскажу о подходе, который поможет реализовывать MVP-функционал практически без участия разработчиков, тем самым сэкономив драгоценное время. Чтобы при желании вы могли применить это у себя, я разработал небольшую библиотеку.
Продукты в OBRIO развиваются очень динамично, и часто приходится реализовывать новый функционал приложений. Но перед тем, как с головой уходить в разработку, любую идею мы должны изначально проверить. Зачем разрабатывать то, что, возможно, не нужно нашему пользователю, верно?
Примерно год назад на нашем приложении Nebula мы запускали раздел консультации астрологов. Это была очередная крутая идея команды (но об этом мы узнали немного позже, конечно же). На тот момент все шло своим чередом: описали задачу, передали на отрисовку дизайнеру. После того, как дизайн был отрисован и утвержден, я взялся реализовывать функционал.
Дизайн выглядел вот так:
Ничего необычного: экран со списком астрологов, профиль астролога, описание услуг, форма для заполнения информации о пользователе. Проблема в том, что экраны профилей астрологов и их услуг немного отличались у каждого специалиста — этим усложнялся UI. Нужно было реализовать множество разных UI-блоков и правильно отрисовать их в зависимости от астролога и его услуг.
Я начал думать, как упростить себе жизнь. Не хотелось тратить несколько дней только на отрисовку всех экранов.
Поскольку профили и услуги астрологов отличались, дизайнер прорисовывал каждый отдельно. Пришла идея: почему бы не отобразить эти экраны с помощью изображения через UIImageView, ведь у меня есть уже полностью готовый экран?! Такой подход мы назвали Image-Driven Development :))
Во время теста идеи нового раздела приложения данные об астрологе и его услугах не менялись и не было предусмотрено никаких анимаций, но остались две проблемы:
- Экран должен выглядеть идентично на любой модели iPhone, без искажений и потери качества.
- На экране есть кнопки, нужно сделать их кликабельными.
Решение первой проблемы было довольно таки простым — я использовал векторные ресурсы, а именно PDF-формат. Все пропорции сохранялись, и качество оставалось неизменным.
Вторая проблема уже более интересная. Сложность была в том, чтобы отлавливать события клика в правильном месте, а именно в области кнопки, но на разных экранах эта область разная, тут и начинается небольшая магия :)
Нам поможет Zeplin, немного математики и невидимая UIButton.
Zeplin
Zeplin — отличный инструмент для совместной работы дизайнеров и разработчиков. Дизайнеры могут легко загружать вайрфреймы, а разработчики — удобно использовать для работы с компонентами.
В нашем случае он поможет определить положение кнопок, которые находятся на изображении.
Конечно же, можно использовать любой другой инструмент, удобный вам, это не принципиально. Например, Figma, Sketch, InVision.
Математика
Итак, ранее мы определили расположение кнопок, теперь, когда мы с помощью Zeplin получили данные по расположению кнопки, а именно ее положение (x, y) и размеры (width, height) относительно изображения на вайрфрейме, нам нужно пересчитать эти данные, чтобы они были корректны относительно любого размера iPhone. Для этого нам нужно вывести 2 коэффициента пересчета — один для горизонтали, другой для вертикали.
let xFactor = currentImageWidth / originalImageWidth
(коэффициент пересчета по горизонтали) = (текущая ширина изображения на iPhone) / (оригинальная ширина изображения с Zeplin)
let yFactor = currentImageHeight / originalImageHeight
(коэффициент пересчета по вертикали) = (текущая высота изображения на iPhone) / (оригинальная высота изображения с Zeplin)
Имея эти коэффициенты, определяем нужные параметры положения кнопки.
let realX = x * xFactor let realY = y * yFactor let realWidth = width * xFactor let realHeight = height * yFactor
UIButton
У нас уже есть все данные по расположению кнопки. Теперь нам нужно иметь возможность обрабатывать нажатия в этой области, для этого мы можем использовать UIButton — это лучше, чем просто отлавливать события нажатия на UIView хотя бы потому, что в UIButton мы можем передавать специфические ивенты отклика, такие как touchUpInside, touchDown и т.д.
UIButton будет точно соответствовать положению кнопки на изображении.
let frame = CGRect(x: realX, y: realY, width: realWidth, height: realHeight)
let button = UIButton(frame: frame)
P.S. Да, можно было и не создавать никаких кнопок, а просто отлавливать координаты нажатия на экране, просто мне показалось так удобнее.
Это все, что нужно для реализации такого подхода.
Для удобства, чтобы каждый раз не заниматься расчётами, я завернул это все в небольшую библиотеку. Вы можете найти ее здесь.
Как пользоваться
Давайте я на реальном примере продемонстрирую, как буквально за несколько минут можно реализовать довольно непростой экран. Для примера возьмём тот же экран профиля астролога. Напомню, выглядит он так:
1. Устанавливаем MagicScreenView
MagicScreenView доступен через CocoaPods. Чтобы установить, просто добавьте эту строчку в ваш Podfile: pod 'MagicScreenView'
2. Добавляем на экран
private let magicScreenView: MagicScreenView = { let screenView = MagicScreenView() screenView.translatesAutoresizingMaskIntoConstraints = false return screenView }()
MagicScreenView — наследник UIView, поэтому можете добавлять его любым удобным для вас способом.
override func viewDidLoad() { super.viewDidLoad() setupLayout() } private func setupLayout() { view.addSubview(magicScreenView) NSLayoutConstraint.activate([ magicScreenView.leftAnchor.constraint(equalTo: view.leftAnchor), magicScreenView.topAnchor.constraint(equalTo: view.topAnchor), magicScreenView.rightAnchor.constraint(equalTo: view.rightAnchor), magicScreenView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) }
2. Определяем положение кнопок
В данном случае нам нужны такие кнопки:
- Profile image: CGRect(x: 141, y: 72, width: 94, height: 94)
- Compatibility: CGRect(x: 16, y: 336, width: 343, height: 38)
- Natal chart: CGRect(x: 16, y: 390, width: 343, height: 38)
- Freeform question: CGRect(x: 16, y: 444, width: 343, height: 38)
- Best time: CGRect(x: 16, y: 498, width: 343, height: 38)
3. Конфигурируем
Создадим объект Profile: это для удобства, чтобы хранить все данные об экране.
struct Profile { enum Action { case profileImage, compatibility, natalChart, freeformQuestion, bestTime var frame: CGRect { switch self { case .profileImage: return CGRect(x: 141, y: 72, width: 94, height: 94) case .compatibility: return CGRect(x: 16, y: 336, width: 343, height: 38) case .natalChart: return CGRect(x: 16, y: 390, width: 343, height: 38) case .freeformQuestion: return CGRect(x: 16, y: 444, width: 343, height: 38) case .bestTime: return CGRect(x: 16, y: 498, width: 343, height: 38) } } var name: String { switch self { case .profileImage: return "Profile image" case .compatibility: return "Compatibility" case .natalChart: return "Natal chart" case .freeformQuestion: return "Freeform question" case .bestTime: return "Best time" } } } let image = UIImage(named: "profileRosalie") let actions: [Action] = [.profileImage, .bestTime, .freeformQuestion, .compatibility, .natalChart] }
Дальше мы используем уже данные с объекта Profile
и конфигурируем MagicScreenView
.
private let profile = Profile() private func setupView() { magicScreenView.configure(withImage: profile.image) let actions = profile.actions.compactMap { action in MagicAction(frame: action.frame, touchAreaInset: 8) { [unowned self] in self.handleAction(action) } } magicScreenView.addActions(actions) }
Метод setupView()
должен вызываться во viewDidLoad()
после setupLayout()
.
4. Обрабатываем нажатия
Для наглядности отобразим поп-ап с информацией о нажатой кнопке.
private func handleAction(_ action: Profile.Action) { let alert = UIAlertController(title: "\(action.name) action", message: "\(action.name) page opened", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Ok", style: .default)) present(alert, animated: true, completion: nil) }
5. Экран готов
6. DebugMode
Можно также активировать debugMode, он подсветит области кнопок.
magicScreenView.isDebugEnabled = true
Собственно, все :)
Такой подход каждый может применить как угодно для разных целей. Важно лишь понимать, что это никак не должно (и не может) заменить полноценную разработку. Я лишь продемонстрировал, как можно просто реализовать MVP-функционал, но тут уже решать вам, пользоваться ли этим.
В дальнейшем я планирую немного доработать библиотеку:
- Добавить возможность конфигурировать через URL, чтобы не хранить ресурсы на клиенте и не увеличивать размер приложения.
- Задавать состояние кнопки — это пригодится для реализации sales-экранов.
6 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів