FrameOk
Вступление
FrameOk – это наш набор инструментов компании MobileUp, который используем при разработке мобильных приложений для платформы iOS.
В него также входит Mutal – полезная утилита для отладки приложения, которая умеет симулировать ошибки сети, автозаполнять поля форм, просматривать логи, менять окружение бэкэнда и запускать кастомные отладочные сценарии.
Применение
FrameOk хорошо сочетается с современными и классическими архитектурами мобильных приложений iOS – например, с Clean Architecture или MVCS.
Установка
Зависимости
Фрейм использует несколько внешних зависимостей с помощью CocoaPods:
pod 'Alamofire'
pod 'AlamofireNetworkActivityLogger'
pod 'Kingfisher'
pod 'PhoneNumberKit'
pod 'XCGLogger'
pod 'GCDWebServer'
pod "SkeletonView"
pod 'SwiftEntryKit'
pod 'InputMask'
Mutal
Включение
Для включения отладочной утилиты и логирования добавьте код в AppDelegate вашего проекта:
class AppDelegate: UIResponder, UIApplicationDelegate {
...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
...
MUDeveloperToolsManager.setup()
...
}
Настройте логику в зависимости от окружения:
MULogManager.isEnabled = isDevelop
MUDeveloperToolsManager.isEnabled = isDevelop
Запуск
Чтобы открыть отладочную панель утилиты встряхните свой тестовый девайс или вызовите команду Shake на симуляторе (cmd + ctrl + z).
Смена окружений
Для динамической смены окружения налету реализуйте протокол DeveloperToolsDelegate:
extension Environments: MUDeveloperToolsDelegate {
// MARK: - Environment
private enum Environment {
static let develop = "develop"
static let production = "production"
}
// MARK: - Public methods
func developerToolsEnvironmentArray() -> [MUEnvironment] {
return [
MUEnvironment(index: Environment.develop, title: "Develop"),
MUEnvironment(index: Environment.production, title: "Production")
]
}
func developerToolsDidEnvironmentChanged(with environment: MUEnvironment) {
switch environment.index {
case Environment.develop : Environments.isProduction = false
case Environment.production : Environments.isProduction = true
default : break
}
}
}
и передайте ссылку на делегат:
MUDeveloperToolsManager.delegate = self
Автозаполнение форм
Для автозаполнения полей в вашем контроллере добавьте такой метод:
func addDebugData(with data: String, to field: UITextField?) {
if MUDeveloperToolsManager.shouldAutoCompleteForms {
field?.value = value
}
}
и вызывайте его в viewDidLoad:
override func viewDidLoad() {
...
addDebugData("[email protected]", to: emailTextField)
...
}
Генерация случайных данных
Добавление к полям форм отладочных значений или генерации случайных данных (электронной почты, телефона, логина, пароля и т.д). Это удобно, например, на экране регистрации нового пользователя:
addDebugData(MUDevelopData.defaultEmail, to: emailTextField)
addDebugData(MUDevelopData.randomLogin, to: loginTextField)
addDebugData(MUDevelopData.randomPassword, to: passwordTextField)
addDebugData(MUDevelopData.randomPhone, to: phoneTextField)
Добавление к полю формы ранее случайно сгенерированных данных (например, на экране авторизации пользователя):
addDebugData(MUDevelopData.previousLogin, to: loginTextField)
addDebugData(MUDevelopData.previousPassword, to: passwordTextField)
Кастомные отладочные сценарии
Часто бывает полезно реализовывать отладочные сценарии, которые позволяют сокращать время воспроизведения прохождения тестовых кейсов.
Пример:
У нас приложение, в котором пользователю необходимо правильно ответить на 200 вопросов. Нам будет удобно спрятать чит-кнопку.
По её нажатию, разработчик должен иметь быстрый доступ к экрану успеха и дальнейшей логике приложения (например, начисления баллов, разблокирование ачивок и тп). Такую кнопку удобно спрятать в отладочную панель.
Для добавления кастомных действий к своему экрану в его контроллере нужно реализовать протокол MUDeveloperToolsCustomActionDelegate:
extension ViewController: MUDeveloperToolsCustomActionDelegate {
func developerToolCustomActionDidTapped(_ developerTools: MUDeveloperToolsController) {
...
}
}
и передать ссылку, например, в viewDidLoad:
MUDeveloperToolsManager.customActionDelegate = self
Запустите приложение, перейдите на нужный экран, откройте отладочную панель и тапните на Custom Action.
Логирование
Отправка сообщений в лог приложения по категориям:
Log.details("...")
Log.event("...")
Log.error("...")
Log.critical("...")
Сетевые ошибки
Симуляция сетевых ошибок из коробки будет работать, если вы использовали для своего сетевого модуля MUDataTransferManager или MUNetworkManager.
Для кастомных сетевых модулей вы можете использовать эти логические свойства и реализовать необходимую логику самостоятельно:
MUDeveloperToolsManager.alwaysReturnConnectionError
MUDeveloperToolsManager.alwaysReturnServerError
MUDeveloperToolsManager.shouldSimulateBadConnection
Например:
if MUDeveloperToolsManager.alwaysReturnConnectionError {
failure(MUNetworkError.connectionError)
}
Сетевой модуль
Вы можете создать свой сетевой модуль на основе базового класса MUDataTransferManager. Он обеспечит работу с сетью, логирование сетевой активности, сериализацию данных, базовую логику авторизации данных и обработки ошибок:
class AppServerClient: MUDataTransferManager {
...
}
Установка headers
Если у вас авторизация Bearer, то вы можете передать request token в свойство token:
token = response.requestToken
Для настройки headers перезапишите метод getHeaders:
override func getHeaders() -> [String : String] {
var headers = super.getHeaders()
headers.setValue(..., forKey: "Authorization")
return headers
}
Обработка ответов
Если вам необходимо реализовать общую логику для обработки полученных ответов от сервера (обработка ошибок, обновление токена и т.п.), то будет удобно перезаписать метод handlerResponse:
override func handlerResponse(
result : Any,
request : MUNetworkRequest?,
recipient : NSObject? = nil,
success : ((Any) -> Void)? = nil,
failure : ((Error?) -> Void)? = nil
) {
guard ... else {
failure?(AppError.parsingError)
return
}
success?(result)
}
Обработка неудачных запросов
В этом методе необходимо реализовать логику конвертации ошибок сети и сериализации данных. Например, привести ошибки к общему enum приложения AppError.
override func handleFailure(
result : Any?,
error : MUNetworkError?,
request : MUNetworkRequest?,
recipient : NSObject?,
completion : ((Error?) -> Void)? = nil
) {
return returnError(
with : AppError.convertNetworkError(error: error ?? MUNetworkError.unknownError),
recipient : recipient,
failure : completion
)
}
Обработка ошибок
Создайте общий enum и перечислите в нём все возможные ошибки, которые могут возникнуть в приложении:
enum AppError: Error, Equatable {
case unknownError
case parsingError
case connectionError
case temporaryNotAvalibleError
case serverError
case lostParameter(String)
...
}
Конвертируйте ошибки других типов в общий enum:
extension AppError {
static func convertNetworkError(error: MUNetworkError) -> AppError {
switch error {
case .connectionError : return AppError.connectionError
case .serverError : return AppError.serverError
case .parsingError : return AppError.parsingError
case .unknownError : return AppError.unknownError
...
}
}
}
Вы может отправлять ошибки из любого места в коде приложения и получать их через NotificationCenter . Для этого добавьте такой код для своего AppError:
extension AppError {
// MARK: - Public properties
static var recipient: NSObject? { didSet { MUErrorManager.recipient = recipient } }
// MARK: - Public methods
static func post(with error: AppError, for recipient: NSObject? = nil) {
MUErrorManager.post(with: error, for: recipient)
}
func post(for recipient: NSObject? = nil) {
MUErrorManager.post(with: self, for: recipient)
}
}
Отправление ошибки:
guard let login = login else {
return AppError.lostParameter("login").post()
}
Получение ошибки с помощью NotificationCenter и добавление её в лог:
NotificationCenter.addObserver(for: self, forName: .appErrorDidCome) { [weak self] notification in
guard let notification = notification.userInfo?["notification"] as? MUErrorNotification else {
return AppError.unknownError.post()
}
guard notification.recipient == self else {
return
}
Log.error("error: \(notification)")
guard let error = notification.error as? AppError else {
return
}
appErrorDidBecome(error: error)
}
Модели данных
Все модели данных должны соответствовать протоколам MUModel, MUCodable:
final class Entity: MUModel, MUCodable {
var primaryKey: String { return id }
...
}
Контроллеры
В состав фрейма входит три базовых контроллера:
- MUViewController
- MUListController
- MUFormController
MUViewController
Все простые экраны проекта нужно наследовать от вашего базового ViewController, который наследуется от MUViewController.
class ViewController: MUViewController
Базовый функционал:
- роутинг
- получение ошибок API по умолчанию
- контейнер над клавиатурой
- показ сообщений и диалогов
- показ индикаторов активности
Роутинг
Инициализация из Storyboard
Название контролера должно совпадать с его Storyboard ID на вкладке Identity в Storyboard:
class CatalogueController: ViewController {
class override var storyboardName: String { return "Catalogue" }
}
Инициализация из Xib
Название xib файла должно совпадать с названием класса:
MUViewController.defaultInstantiateMethod = .fromNib
Получение инстанса
CatalogueController.instantiate()
Навигация
Переход к контроллеру из другого контроллера:
push(with: CatalogueController.self) { instance in
instance.productId = productId
}
Презент контроллера в другом контроллере:
present(with: CatalogueController.self) { instance in
instance.productId = productId
}
Презент контроллера в другом контроллере со своей навигацией:
present(with: CatalogueController.self, withNavigation: true)
Вставка контроллера во view другого контроллера:
insert(controller: CatalogueController.instantiate(), into: self.view)
remove(child: childrenController)
Обработка ошибок
Если ошибки были отправлены с помощью NotificationCenter, то их может поймать MU контроллер из коробки.
Получение ошибки в классе базового контроллера, наследуемого от MUViewController:
override func appErrorDidBecome(error: Error) {
guard let error = error as? AppError else {
return
}
appErrorDidBecome(error: error)
}
func appErrorDidBecome(error: AppError) {
}
Далее уже в контроллере экрана можно обрабатывать ошибку:
override func appErrorDidBecome(error: AppError) {
...
}
Отключить получение ошибок для контроллера:
override var isErrorRecipient: Bool { false }
Показ нативного алерта
showPopup(
title : "Error",
message : AppError.unknownError.localizedDescription,
buttonTitle : "Ok",
action : { ... }
)
Показ нативного алерта с кнопками
showDialogAlert(
title : "Error",
message : AppError.unknownError.localizedDescription,
leftButtonTitle : "Ok",
rightButtonTitle : "Cancel",
leftButtonStyle : .default,
rightButtonStyle : .cancel,
leftButtonAction : { ... },
rightButtonAction : { ... }
)
Показ тоста
showToast(
title : "Connection error",
message : AppError.connectionError.localizedDescription,
duration : 2
)
Показ кастомной вью
show(
customView : CustomAlert.instantiate(),
position : .center,
animationType : .fade
)
Показ контроллера
show(controller: CatalogueController.instantiate())
Показ нижнего модального окна
showBottomPopup(
controller : TransactionController.instantiate(),
backgroundColorStyle : backgroundColorStyle,
arrowIcon : R.image.common.icomCloseBottomPopup(),
arrowIconOffset : 8
)
Закрыть все попапы
popupControl.closeAll()
Проверка видимости попапа
show(controller: CatalogueController.instantiate(), popupName: "CataloguePopup")
popupControl.isCurrentDisplaying(popupName: "CataloguePopup")
Показ индикатор загрузки
isLoading = true
Настройка индикатора
MUActivityIndicatorControl.defaultStyle = .dark
indicatorControl.style = .lightLarge
indicatorControl.defaultDelay = 0.6
Показ скелетной анимации
indicatorControl.isEnabled = false
loadControl.isEnabled = true
Настройка скелетной анимации
MULoadControl.multilineCornerRadius = 5
MULoadControl.multilineHeight = 15
MULoadControl.multilineLastLineFillPercent = 70
MULoadControl.gradientBaseColor = .white
Настройка скелетной анимации вручную
loadControl.isManualSkeletonable = true
loadControl.shouldCreateOfEmptyItems = false
Контейнер над клавиатурой
Для закрепления UI элементов над клавиатурой (например кнопки) с её анимацией показа и скрытия:
keyboardControl.containerView = keyboardContainer
или можно использовать IBOutlet в xib или storyboard вашего контроллера:
IB keyboardContainer
Другие настройки MUViewController
Добавление скролла
Автоматически добавит скролл, если текущий контроллер не помещается на экране девайса по высоте:
override var hasScroll: Bool { true }
Настройка навигации
override var hasNavigationBar: Bool { false }
Удалит контроллер из списка navigationController.viewControllers после показа другого экрана:
override var shouldRemoveFromNavigation: Bool { true }
Управление нативным жестом для перехода к предыдущему экрану:
override var interactivePopGestureEnabled: Bool { false }
Проверка видимости:
guard isVisible, isFirstAppear else { return }
Нотификации
Подписание на нотификации, отправленные с помощью NotificationCenter:
override func subscribeOnCustomNotifications() {
NotificationCenter.addObserver(for: self, forName: .screenHistoryTransactionDidSuccess) { [weak self] _ in
self?.requestObjects()
}
}
MUFormController
Все экраны с полями ввода проекта нужно наследовать от вашего базового FormController, который наследуется от MUFormController:
class FormController: MUFormController
Базовый функционал:
- валидация данных
- организация работы с полями и кнопки отправки
- переходы между полями
- блокировка кнопки отправки
Валидация
Поля ввода формы должны соответствовать протоколу:
protocol VerifyFieldProtocol {
var value: String { set get }
var isError: Bool { set get }
var errorMessage: String? { set get }
func setError(on: Bool, message: String?)
}
Добавления правил валидации для поля:
addVerify(
field. : passwordField,
rules. : [.required, .minLength(8)],
message : "Длина пароля должна быть не менее 8 символов".localize
)
Список правил валидации
enum MUValidateRule {
case required
case email
case numeric
case numericFloat
case minLength(Int)
case maxLength(Int)
case minValue(Int)
case maxValue(Int)
case allowChar(String)
case allowRegexp(String)
case containsAtLeastOneOf([MURegexpClass])
}
Проверка и отправка формы
Проверка формы на валидность и заполненность:
guard isValid, isFilled else { return }
Метод для дополнительной кастомной валидации:
override func customValidate() -> Bool
Метод, который будет вызван после валидации:
override func afterValidate()
Метод отправки данных для валидной формы:
override func submitForm()
IBOutlet для назначения кнопки отправки данных. Для неё будет реализована логика блокировки и разблокировки:
IB submitButton
По нажатию на кнопку продолжить не будет вызван метод submitForm:
IB continueButton
Настройка валидации
override var fieldsValidation: ValidationOption { .filledOnly }
Доступные опции для настройки:
enum ValidationOption: String {
case all, filledOnly, activeFieldOnly
}
MUListController
Все простые экраны проекта нужно наследовать от вашего базового ListController, который наследуется от MUListController.
class ListController: MUListController
Базовый функционал:
- подгрузка данных с сети
- группировка данных
- анимация ячеек таблиц
- кэширование данных в файл
- обновление списка по pull to refresh
- подгрузка данных infinite scrolling
- показ пустых состояний empty states
Настройка таблицы
Назначить IBOutlet в xib или storyboard для текущего контроллера:
IB tableView: UITableView?
IB collectionView: UICollectionView?
Добавить ячейку и ее xib файл:
class ArticleCell: MUTableCell {
// MARK: - Override methods
override func setup(with object: MUModel, sender: Any? = nil) {
super.setup(with: object, sender: sender)
...
}
}
Зарегистрировать ячейку из xib:
registerNib(of: ListCell.self)
Если нужно назначить индификатор ячейки, в зависимости от типа данных:
override func cellIdentifier(for object: MUModel, at indexPath: IndexPath) -> String?
Анимация и добавление данных
Добавление анимации для только таблиц UITableView:
tableControl.isAnimated = true
tableControl.animationStyle= .fade
В модели данных обязательно должен быть назначен id:
final class Entity: MUModel, MUCodable {
var primaryKey: String { return id }
...
}
Добавить данные в таблицу или коллекцию:
objects = items
objects.append(item)
Запрос данных из сети
Запрос данных из сети нужно реализовать в методе:
override func beginRequest()
Запросить данные с показом лоадера или скелетной анимации на экране:
requestObjects(withIndicator: true)
Обновить данные и завершить показ лоадера или скелетной анимации:
update(objects: items)
Добавить обновление данных по Pull to Refresh:
override var hasRefresh: Bool { true }
Пагинация с бесконечной прокруткой
Добавить поддержку Infinite scrolling:
override var hasPagination: Bool { true }
Получить текущую страницу:
let page = paginationControl.page
Пример запроса данных с пагинацией:
override func beginRequest() {
interactor.getNews(id: tag.id, page: paginationControl.page) { [weak self] (items) in
self?.update(objects: items)
}
}
Кэширование данных в файл:
Включить поддержку кэширования:
override var hasCache: Bool { true }
Подготовка модели для кэширования:
extension Product {
static let cacheControl: MUCacheControlProtocol = MUCacheControlManager.get(for: Product.self)
}
Добавить кэширование:
override var cacheControl: MUCacheControlProtocol? { return Product.cacheControl }
Сохранение и загрузка данных в кэш:
cacheControl.save()
cacheControl.load()
Расширения
Форматирование
Для удобства, всё форматирование строк сделано через расширение базового класса String
Даты и время
Время:
String.format(time: Date())
String.format(time: Date(), style: .positional, units: [.hour, .minute, .second])
Даты:
String.format(date: Date(), format: String = "d MMM, HH:mm")
Числа и валюта
Числа:
String.format(number: number, minMantissa: 0, maxMantissa: 4)
Валютные числа:
String.format(price: price)
String.format(rub: priceInRub)
Телефоны
Телефон:
String.format(phone: phone)
String.format(phone: phone, to: .e164, onlyNumbers: true)
String.currentPhoneCoutryCode
Регулярные выражения
Проверка:
guard String.check(email, regexp: "[0-9a-z]+") else { ... }
Поиск:
let marches = targetString.matches(for: "[a-zA-Z]+")
Замена:
let string = rawString.replace(pattern: "[\s]+", with: "")
Маски
Добавление маски:
String.mask(template: "+7 999 999 99 99", value: phone)
Требования
- Swift 4.2+
- iOS 9.0+
Установка
CocoaPods
Add the following to Podfile
:
pod 'FrameOk'
Carthage
Add the following to Cartfile
:
github "MobileUpLLC/FrameOk"
Manual
Загрузите и перетащите файлы из исходной папки в свой проект Xcode
License
FrameOk is distributed under the MIT License.