FrameOk – это созданный MobileUp набор инструментов, который мы используем при создании мобильных приложений на платформе iOS. Этот фреймворк ускоряет процесс разработки за счет универсальных базовых компонентов. Благодаря им можно не тратить время на проработку рутинных вещей, а уделить внимание качеству кода, созданию анимации, подготовке фич и т.д. По нашим подсчетам, использование FrameOk в сочетании с рефакторингом, статическим анализом и строгим код-ревью позволяет закладывать 5-7 лет на эксплуатацию кода. Это заметно снижает стоимость поддержки кода, который не требует переработки с нуля.
В состав фрейма также входит панель разработчика 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'
Для включения отладочной утилиты и логирования добавьте код в 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("test@test.com", 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 {
...
}
Если у вас авторизация 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
Все простые экраны проекта нужно наследовать от вашего базового ViewController, который наследуется от MUViewController.
class ViewController: MUViewController
- роутинг
- получение ошибок API по умолчанию
- контейнер над клавиатурой
- показ сообщений и диалогов
- показ индикаторов активности
Название контролера должно совпадать с его Storyboard ID на вкладке Identity в Storyboard:
class CatalogueController: ViewController {
class override var storyboardName: String { return "Catalogue" }
}
Название 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
Автоматически добавит скролл, если текущий контроллер не помещается на экране девайса по высоте:
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()
}
}
Все экраны с полями ввода проекта нужно наследовать от вашего базового 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
}
Все простые экраны проекта нужно наследовать от вашего базового 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+
Add the following to Podfile
:
pod 'FrameOk'
Add the following to Cartfile
:
github "MobileUpLLC/FrameOk"
Загрузите и перетащите файлы из исходной папки в свой проект Xcode
FrameOk is distributed under the MIT License.