Image-Driven Development: как минимизировать время разработчиков при реализации MVP-функционала

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привет, меня зовут Сергей Матвеев. Я iOS Lead в компании OBRIO, которая входит в экосистему бизнесов Genesis и занимается разработкой мобильных приложений и игр. В этой статье я расскажу о подходе, который поможет реализовывать MVP-функционал практически без участия разработчиков, тем самым сэкономив драгоценное время. Чтобы при желании вы могли применить это у себя, я разработал небольшую библиотеку.

Продукты в OBRIO развиваются очень динамично, и часто приходится реализовывать новый функционал приложений. Но перед тем, как с головой уходить в разработку, любую идею мы должны изначально проверить. Зачем разрабатывать то, что, возможно, не нужно нашему пользователю, верно?

Примерно год назад на нашем приложении Nebula мы запускали раздел консультации астрологов. Это была очередная крутая идея команды (но об этом мы узнали немного позже, конечно же). На тот момент все шло своим чередом: описали задачу, передали на отрисовку дизайнеру. После того, как дизайн был отрисован и утвержден, я взялся реализовывать функционал.

Дизайн выглядел вот так:

Ничего необычного: экран со списком астрологов, профиль астролога, описание услуг, форма для заполнения информации о пользователе. Проблема в том, что экраны профилей астрологов и их услуг немного отличались у каждого специалиста — этим усложнялся UI. Нужно было реализовать множество разных UI-блоков и правильно отрисовать их в зависимости от астролога и его услуг.

Я начал думать, как упростить себе жизнь. Не хотелось тратить несколько дней только на отрисовку всех экранов.

Поскольку профили и услуги астрологов отличались, дизайнер прорисовывал каждый отдельно. Пришла идея: почему бы не отобразить эти экраны с помощью изображения через UIImageView, ведь у меня есть уже полностью готовый экран?! Такой подход мы назвали Image-Driven Development :))

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

  1. Экран должен выглядеть идентично на любой модели iPhone, без искажений и потери качества.
  2. На экране есть кнопки, нужно сделать их кликабельными.

Решение первой проблемы было довольно таки простым — я использовал векторные ресурсы, а именно 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. Определяем положение кнопок

В данном случае нам нужны такие кнопки:

  1. Profile image: CGRect(x: 141, y: 72, width: 94, height: 94)
  2. Compatibility: CGRect(x: 16, y: 336, width: 343, height: 38)
  3. Natal chart: CGRect(x: 16, y: 390, width: 343, height: 38)
  4. Freeform question: CGRect(x: 16, y: 444, width: 343, height: 38)
  5. 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-функционал, но тут уже решать вам, пользоваться ли этим.

В дальнейшем я планирую немного доработать библиотеку:

  1. Добавить возможность конфигурировать через URL, чтобы не хранить ресурсы на клиенте и не увеличивать размер приложения.
  2. Задавать состояние кнопки — это пригодится для реализации sales-экранов.
👍НравитсяПонравилось12
В избранноеВ избранном3
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Серега, красавчик. Делали когда-то так давно, еще на первой работе, но ты догадался это упаковать в библиотеку и загрузкой данных с бека, так можно легко ранить сплит-тесты всевозможные как у вас любят и не редоплоить апку.

Когда-то давно, ещё до мобильной разработки, делал такую же систему для видеоигры в жанре «поиск предметов». Написал плагин для Photoshop, который экспортировал слои всех предметов в отдельные векторные изображения и создавал json-файл, описывающий прямоугольники всех слоев на координатной сетке экрана. Дальше простенький движок, который читал этот json и изображения с диска и расставлял их по своим местам с учетом разрешения экрана. Обработка нажатий на слои тоже сверялась с прямоугольниками из json-файла. Абсолютно всё автоматизоровано. Все, что нужно — это макет для Photoshop и один клик.
Я не вижу смысла в том, чтобы вручную прописывать координаты, как у вас в 3-м разделе. Это должна быть просто тулза, которая одним кликом превращает проект в Photoshop/Sketch/Figma в полностью сконфигурированный Xcode проект. Только заполни на свое усмотрение содержание сгенерированных функций, вызываемых обработчиком нажатий.

Интересное решение, спасибо что делитесь! На то время такой вариант реализации был максимально понятным, быстрым и удобным.
Возьмём себе на заметку)

А в чем профит такого прототипа на Swift? В фигме ж есть достаточно функциональные интерактивные прототипы, в конце концов есть инструменты вроде Mockplus с еще большими возможнеостями создания интерактивных прототипов.

Насколько я понял Mockplus это инструмент, который используется дизайнерами просто для прототипирования. Я с ним не знаком, поправьте если я что-то не понял.

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

Подписаться на комментарии