Привет, Хабровчане!
Меня зовут Дмитрий Новиков, я – разработчик департамента разработки корпоративных решений в 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*.
Таким образом мы убиваем сразу нескольких зайцев:
-
Фреймворк поставляется в виде Binary Target внутри SPM пакета, что позволяет экономить время на сборке проекта, к которому прилинкован фреймворк;
-
SPM не тратит время на линковку зависимостей и их индексацию, в частности при большом количестве исходных файлов на один пакедж;
-
Мы можем запаковать сразу несколько архитектур внутри одного пространства имён, чтобы иметь поддержку как 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, например, для отладки приложения на этапе тестирования, чтобы было понимание, какие версия и билд используются в приложении на момент теста. Для этих целей были созданы пара классов, также конформящихся к протоколам и реализующих функциональность по получению версии и номера сборки.
Пример обращения к сервису 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, мы смогли реализовать необходимые нам паттерны буквально за день.
По итогу мы получили такую пару графиков:
Сами графики очень гибки в настройке и очень просты в использовании; например, чтобы скруглить отображение секций, достаточно просто указать Cap Style, выбрав нужное значение из перечисленных. Данные графики хорошо подойдут, если вам в проекте необходимо вывести множество статистической информации и при этом как-то графически отделять между собой эту информацию в виде паттернов.
При разработке этих графиков мы задались вопросом, как их лучше применять на практике. До их разработки в нашем проекте лежало порядка 2–3 сторонних библиотек, морально устаревших, неподдерживаемых долгое время, а также не имеющих такую гибкую систему кастомизации. Реализовав набор из пары пайчартов, мы избавились от лишних зависимостей, и у нас появилась возможность самим определять дальнейшую функциональность графиков, а также расширять её.
IBSBlurVisualFX & IBSVibrancyVisualFX
Первый класс позволяет задавать степень размытия своего слоя. Второй – упрощает работу с Vibrancy Effect для добавляемого набора отображений и подкапотно использует класс IBSBlurVisualFX, также позволяющий задавать степень размытия добавленных отображений.
Controllers
IBSSplitSpaceController
Является аналогом класса UISplitViewController от Apple, но с некоторыми улучшениями. Позволяет задавать размер и первоначальное положение сайдбара, а также определяет метод, который свайпом по сайдбару сворачивает и разворачивает его. Метод имеет уровень доступа open и может быть переопределен на проде, либо если необходимы другие поведения при свайпе, либо при нажатии на левую часть Split Space Controller’а.
Заключение
Во время подготовки к выпуску нашего фреймворка мы получили ценный практический опыт создания необходимой функциональности и разработки удобного API для использования в сторонних приложениях. Разумеется, некоторые из реализованных нами фич уже есть в сторонних фреймворках, однако не всегда удобно подключать десяток зависимостей к проекту и следить за актуальностью каждого из них.
Мы планируем расширять функциональность нашего решения и в наших ближайших планах разработать:
-
IBSExtendedTabBarController — аналог UITabBarController от Apple, но с определёнными дополнениями и набором других поведений. Он позволяет независимо от открытой вкладки схлопнуть всё по нажатию на центральную табу или запрезентить некоторый контроллер, не помещая его в основной стек контроллеров. Также у него есть кнопка «назад», которая анимировано появляется и исчезает в левой части таб бара на уровне с иконками вкладок;
-
IBSLogger — аналог большинства сервисов по логированию данных как на устройстве, так и с доступом в сеть, с удобным API для работы, оптимальным временем чтения и записи логов в память, а также оптимизированный под носимые устройства по уровню потребления ресурсов.
Спасибо за внимание.
P.S. Наши наработки на Github:
ссылка на оригинал статьи https://habr.com/ru/company/ibs/blog/712162/
Добавить комментарий