FrameOk 1.0.0

FrameOk 1.0.0

Maintained by Nikolai Timonin, Eugene Valeev.



 
Depends on:
Alamofire~> 4.9
AlamofireNetworkActivityIndicator>= 0
AlamofireNetworkActivityLogger>= 0
Kingfisher>= 0
PhoneNumberKit>= 0
XCGLogger>= 0
GCDWebServer~> 3.0
SkeletonView>= 0
SwiftEntryKit>= 0
InputMask~> 4.3.0
 

FrameOk 1.0.0

  • By
  • Dmitry Smirnov, MobileUp, Maxim Aliev, MobileUp, Ilia Biltuev, MobileUp, Nikolai Timonin, MobileUp and Pavel Petrovich, MobileUp

FrameOk

Swift5 Platform iOS CocoaPods compatible Carthage compatible License: MIT

Вступление

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.