Нельзя просто так взять и написать полезный iOS фреймворк… Или можно?

от автора

Привет, Хабровчане!

Меня зовут Дмитрий Новиков, я – разработчик департамента разработки корпоративных решений в IBS. Мы в компании занимаемся разработкой мобильных приложений на заказ и хотим рассказать, как мы пришли к идее создания собственного iOS фреймворка для решения ряда полезных в мобильной разработке задач, а также что у нас в итоге из этого получилось.

Как у нас возникла идея создать собственный iOS фреймворк?

В рамках работы над разными проектами было разработано множество полезных функциональных модулей, которые решали актуальные задачи. Модули создавались обособленно друг от друга и представляли собой самодостаточные единицы, решающие конкретную поставленную задачу. При этом на каждом новом проекте вновь реализовывать ту же самую логику, например, для получения спецификации устройства или же для использования Canvas в UIKit проекте, нам не очень-то и хотелось. К тому же в этот момент мы заканчивали переход от менеджера зависимостей CocoaPods к Swift Package Manager (далее – SPM).

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

Задавшись вопросом хардверных и софтверных ограничений, мы пришли к выводу, что минимальными поддерживаемыми версиями iOS и iPadOS для нашего фреймворка станут iOS 13.0+ и iPadOS 13.0+, соответственно. Также решили, что не хотим поддерживать legacy, и хотим своевременно актуализировать исходный код фреймворка, поэтому минимальной версией IDE для сборки исходников фреймворка стал Xcode 14.0+, а также версия компилятора, поддерживающего Swift 5.7+.

В ряде случаев SPM создаёт некоторые проблемы сборки с исходным кодом пакеджей, перестаёт нормально собирать проект, а также индексирует и пересобирает зависимости бóльшую часть времени. В связи с этим было решено создать таргет Xcode Workspace с исходным кодом, который впоследствии билдился бы в Xcode Fat Framework*.

Таким образом мы убиваем сразу нескольких зайцев:

  1. Фреймворк поставляется в виде Binary Target внутри SPM пакета, что позволяет экономить время на сборке проекта, к которому прилинкован фреймворк;

  2. SPM не тратит время на линковку зависимостей и их индексацию, в частности при большом количестве исходных файлов на один пакедж;

  3. Мы можем запаковать сразу несколько архитектур внутри одного пространства имён, чтобы иметь поддержку как iOS и iPadOS устройств, так и симуляторов для x86_64 и arm64 архитектур.

Xcode Fat Framework: фреймворк поставляется в виде двоичного файла, тем самым упрощая сборку проектов, в которых он используется, и обеспечивая поддержку необходимого набора архитектур как для реальных устройств, так и для симуляторов под платформы Apple и Intel.

В итоге мы сели, прикинули набор необходимых фич, которые хотелось вынести в отдельные сущности, и получили законченный и готовый к использованию SDK с необходимым набором функционала.

Фреймворк включает следующую функциональность:

  • Development Tools

    • Device Specification

    • Live Preview

    • Project Specifications

  • Hardware

    • Haptic Feedback

    • Video Player

  • Software

    • General Purpose Types

      • Data Types

      • Structure Types

    • Design System

      • Components

      • Controllers

Development Tools

Device Specification

Данный класс является базовым сервисом, который нам в целом не хотелось таскать из проекта в проект, но без которого не обойтись при реализации сервиса аналитики, логирования ошибок и модуля синхронизации с сервером. Этот сервис реализует возможность получения информации об устройстве, на котором запущено приложение. Например, модель, тип, имя устройства, версия и тип операционной системы и т.п. Данный сервис также имеет протокол, который он реализует, чтобы была возможность совместимости с точки зрения использования IoC контейнеров.

Пример обращения к сервису IBSDevice для получения свойств устройства:

let specification = IBSDevice.current.specification  print(specification.device.name) // iPhone 14 Pro

Live Preview

Сразу оговорим, что хоть мы и используем SwiftUI, но далеко не на всех проектах, а переводить всю старую кодовую базу с UIKit на SwiftUI мы пока не готовы, но при этом Canvas, пришедший к нам с Xcode 11.0 и iOS 13.0, нам хотелось использовать уже здесь и сейчас. По этой причине у нас возникла мысль совместить всё лучшее из двух миров – продолжать вести разработку с использованием UIKit, но при этом пользоваться Canvas из SwiftUI в UIKit проекте. Поэтому мы написали расширения для классов UIViewController и UIView, дополненные методом livePreview(), который позволяет нам конформить эти два класса к протоколу View из SwiftUI и пользоваться всеми благами Canvas и Hot Reload.

Пример кода с реализацией красного квадрата:

import UIKit import IBSKit   final class ViewController: UIViewController {     private let squareView: UIView = {         let view = UIView()         view.backgroundColor = .red         view.isUserInteractionEnabled = false         view.clipsToBounds = true         view.translatesAutoresizingMaskIntoConstraints = false         return view     }()       override func viewDidLoad() {         super.viewDidLoad()           setupViews()           makeLayout()     }       private func setupViews() {         view.addSubview(squareView)     }       private func makeLayout() {         NSLayoutConstraint.activate([             squareView.centerXAnchor.constraint(equalTo: view.centerXAnchor),             squareView.centerYAnchor.constraint(equalTo: view.centerYAnchor),             squareView.widthAnchor.constraint(equalToConstant: 100),             squareView.heightAnchor.constraint(equalToConstant: 100)         ])     } }  #if DEBUG && canImport(SwiftUI) import SwiftUI   @available(iOS 15.0, *) struct ViewController_Preview: PreviewProvider {     static var previews: some View {         ViewController()             .livePreview()             .ignoresSafeArea()     } } #endif

Также пример взаимодействия с Live Preview можно посмотреть на более сложных экранах, реализованных в проекте IBSKit Demo, демонстрирующем функциональность фреймворка.

Производительность {проблемы}

И всё бы хорошо, но при запуске Live Preview приходилось ждать больше минуты. Данная проблема возникала при использовании на крупных проектах и была связана с оптимизацией самой IDE в связке с работой нативного рендерера Canvas. С приходом Xcode 14.0 Apple удалось исправить большинство проблем при перерисовке Canvas, где теперь как минимум Canvas обновляется сам и нет необходимости нажимать кнопку Resume. А ещё фреймворк стал поставляться не в виде исходного кода, а в виде бинарника, что сокращало время на «холодную» сборку проекта.

Project Specifications

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

Project Specifications
Project Specifications

Пример обращения к сервису IBSSDK для получения информации о версии фреймворка:

let version = IBSSDK.info.version let build = IBSSDK.info.build   print("SDK Ver. \(version.major).\(version.minor).\(version.patch)") // SDK Ver. 1.1.2   print("SDK Build (build)") // SDK Build 11

Hardware

Haptic Feedback

Данный сервис был создан для использования в проектах, разрабатываемых под iPhone. Он позволяет использовать Taptic Engine без использования обёрток для согласования типа устройства, а также по дефолту поддерживает многопоточность. Чтобы заюзать вибротклик, достаточно обратиться к методу:

execute(with: IBSHaptic.FeedbackType)

Например:

IBSHaptic.feedback.execute(with: .success)

Video Player

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

Ряд базовых методов работы с плеером включает:

  • isPlaying() Bool. Позволяет узнать, воспроизводит ли плеер видеоряд;

public func isPlaying() -> Bool {     guard let player else { return false }       return player.rate > 0 }
  • play() Void. Позволяет начать воспроизведение видеоряда;

public func play() {     guard         let player,         player.currentItem?.status == .readyToPlay     else { return }       player.play()     player.rate = rate }
  • pause() Void. Позволяет поставить видеоряд на паузу;

public func pause() {     guard let player else { return }       player.pause() }
  • seekToPosition(seconds:) → Void. Позволяет поставить скраббер на конкретную секунду видеоряда. Можно использовать для реализации перемотки видеоряда.

public func seekToPosition(seconds: Float64) {     guard let player else { return }       pause()       guard let timeScale = player.currentItem?.asset.duration.timescale else { return }       player.seek(         to: .init(             seconds: seconds,             preferredTimescale: timeScale         )     ) { [weak self] _ in         guard let self else { return }           self.play()     } }

Также данный класс включает набор методов делегата для работы с видеоплеером:

// Позволяет узнать прогресс воспроизведения видеоряда. // Например, когда видеоряд загружается из сети и необходимо знать, // какой прогресс видеоряда уже был проигран, // чтобы можно было запросить следующий пакет данных, // а также закэшировать предыдущий. func downloadedProgress(with progress: Double)  // Информирует о том, что видеоряд готов к воспроизведению. func readyToPlay()  // Информирует о том, что прогресс был обновлен. func didUpdateProgress(with progress: Double, and currentTime: Double?)  // Информирует о том, // что некоторый переданный видеоряд для воспроизведения // был успешно воспроизведен. func didFinishPlayItem()  // Информирует о том, // что некоторый переданный видеоряд для воспроизведения // не был успешно воспроизведен. func didFailPlayToEnd() 

Software

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

Наша софтверная часть была разделена на два элемента: типы общего назначения и сама дизайн-система, включающая в себя набор UI компонентов.

General Purpose Types

Data Types

Data Types представляют собой набор типов данных недоступных по дефолту в языке Swift, но необходимых при реализации кастомных компонентов вроде навигации, сохранения и обработки данных по разным условиям и с разным результатом работы, добавления в очередь, конечной обработки элементов и способов доступа к ним.

Structure Types

Structure Types – типы, представляющие собой набор структур, перечислений и т.п. для реализации атрибутов текста, отступов, применения множества стилей, используемых в качестве типов свойств, реализуемых внутри классов UI компонентов.

Design System

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

Components

IBSIndentedLabel

Наследник UILabel, который расширяет функциональность родительского класса, предоставляя возможность задавать padding к тексту наподобие того, как это происходит в SwiftUI.

IBSCountdownView

Класс, позволяющий задать количество секунд для отсчета и запустить таймер; вьюха выезжает снизу экрана и отсчитывает каждую секунду, при этом исполняя виброотдачу на базе класса, описанного ранее. Включает в себя метод делегата, передающий управление обратно при окончании отсчёта для дальнейших действий. Например, данный класс можно использовать для таймера перед запуском тренировки, как это было реализовано в приложении Nike + iPod во времена iOS 6.

Интересной частью данного класса является метод startTimer(with:), реализующий таймер с передачей управления по завершении, а также сам обработчик таймера handleTimer(timer:) с отсчётом:

public func startTimer(with time: UInt16) {     estimatedTime = time       let timer = Timer(         timeInterval: 1.0,         target: self,         selector: #selector(self.handleTimer(timer:)),         userInfo: nil,         repeats: true     )        // Переключение режима работы ранлупа     // для непрерывного выполнения обработчика таймера     // вне зависимости от взаимодействия с UI интерфейсом iOS приложения.     RunLoop.current.add(timer, forMode: .common) }
@objc private func handleTimer(timer: Timer) {     countdownLabel.text = "\(estimatedTime)"       if estimatedTime == 0 {         // Инвалидация таймера по завершении         timer.invalidate()           IBSHaptic.feedback.execute(with: .success)                // Вызов метода делегата,          // в котором необходимо реализовывать логику         // по окончании работы таймера         delegate?.countdownDidFinished()     } else {         // Отсчет таймера         estimatedTime -= 1           IBSHaptic.feedback.execute(with: .soft)     } }

IBSPlayerView

Класс, являющийся подложкой-представлением для отображения видеоряда для IBSVideoPlayer. Передаётся в качестве параметра в инициализатор IBSVideoPlayer, куда видеоплеер рендерит последовательность видеоряда. Сам класс представляет собой наследника UIView и может быть размещен как subview на superview при помощи фреймов или auto layout.

IBSDonutChart & IBSPieChart

Особенно интересная часть. Необходимость создания кастомного Pie Chart возникла при разработке одного iPad проекта, в котором нужно было вывести большое количество статистической информации – таблицы, графики различных видов, фильтры и т.п. И вот в очередном спринте аналитик и дизайнер обрадовали нас новой задачей.

Необходимо было реализовать график, который:

  • умел бы базово выводить информацию по разным категориям,

  • имел бы графический паттерн для разделения данных категорий между собой,

  • мог бы автоматически закруглять края,

  • был бы способен уменьшать радиус total view,

  • умел бы выводить total text в центре графика.

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

По итогу мы получили такую пару графиков:

IBSPieChart
IBSPieChart
IBSDonutChart
IBSDonutChart

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

При разработке этих графиков мы задались вопросом, как их лучше применять на практике. До их разработки в нашем проекте лежало порядка 2–3 сторонних библиотек, морально устаревших, неподдерживаемых долгое время, а также не имеющих такую гибкую систему кастомизации. Реализовав набор из пары пайчартов, мы избавились от лишних зависимостей, и у нас появилась возможность самим определять дальнейшую функциональность графиков, а также расширять её.

IBSBlurVisualFX & IBSVibrancyVisualFX

Первый класс позволяет задавать степень размытия своего слоя. Второй – упрощает работу с Vibrancy Effect для добавляемого набора отображений и подкапотно использует класс IBSBlurVisualFX, также позволяющий задавать степень размытия добавленных отображений.

IBSBlurVisualFX
IBSBlurVisualFX
IBSVibrancyVisualFX
IBSVibrancyVisualFX

Controllers

IBSSplitSpaceController

Является аналогом класса UISplitViewController от Apple, но с некоторыми улучшениями. Позволяет задавать размер и первоначальное положение сайдбара, а также определяет метод, который свайпом по сайдбару сворачивает и разворачивает его. Метод имеет уровень доступа open и может быть переопределен на проде, либо если необходимы другие поведения при свайпе, либо при нажатии на левую часть Split Space Controller’а.

IBSSplitSpaceController
IBSSplitSpaceController

Заключение

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

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

  • IBSExtendedTabBarController — аналог UITabBarController от Apple, но с определёнными дополнениями и набором других поведений. Он позволяет независимо от открытой вкладки схлопнуть всё по нажатию на центральную табу или запрезентить некоторый контроллер, не помещая его в основной стек контроллеров. Также у него есть кнопка «назад», которая анимировано появляется и исчезает в левой части таб бара на уровне с иконками вкладок;

  • IBSLogger — аналог большинства сервисов по логированию данных как на устройстве, так и с доступом в сеть, с удобным API для работы, оптимальным временем чтения и записи логов в память, а также оптимизированный под носимые устройства по уровню потребления ресурсов.

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

P.S. Наши наработки на Github:


ссылка на оригинал статьи https://habr.com/ru/company/ibs/blog/712162/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *