FrameOk 1.0.0

FrameOk 1.0.0

Nikolai TimoninEugene Valeev维护。



 
依赖
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

  • Dmitry Smirnov,MobileUp,Maxim Aliev,MobileUp,Ilia Biltuev,MobileUp,Nikolai Timonin,MobileUp和Pavel Petrovich,MobileUp创作

FrameOk

Swift5 Platform iOS CocoaPods compatible Carthage compatible License: MIT

Вступление

FrameOk 是MobileUp公司开发的一组工具集,用于开发iOS平台上的移动应用程序。

此外,还包括 Mutal —— 一个用于调试应用程序的实用工具,可以模拟网络错误,自动填充表单字段,查看日志,更改后端环境,并运行自定义调试脚本。

Применение

FrameOk 可以很好地与现代和经典iOS移动应用程序架构相配合——例如,与 Clean ArchitectureMVCS

Установка

依赖项

框架使用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("...")

网络错误

如果您使用了网络模块的 MUDataTransferManagerMUNetworkManager,则默认会模拟网络错误。

对于自定义网络模块,您可以使用这些逻辑属性并自行实现必要逻辑

MUDeveloperToolsManager.alwaysReturnConnectionError
MUDeveloperToolsManager.alwaysReturnServerError
MUDeveloperToolsManager.shouldSimulateBadConnection

例如

if MUDeveloperToolsManager.alwaysReturnConnectionError {

    failure(MUNetworkError.connectionError)
}

网络模块

您可以根据基础类 MUDataTransferManager 创建自己的网络模块。这将支持网络操作,记录网络活动,数据序列化,基本数据授权逻辑和错误处理

class AppServerClient: MUDataTransferManager {
    ...
}

安装 headers

如果您使用 Bearer 进行授权,则可以在 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中的身份标识符标签页上的Storyboard ID匹配

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)

将控制器插入到另一个控制器的视图

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

或者可以在您的控制器中的xib或storyboard中使用IBOutlet

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

所有带有输入字段的屏幕都需要继承你自定的 baseUrl Contoller,它继承自 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

基本功能

  • 从网络加载数据
  • 数据分组
  • 表单元格动画
  • 将数据缓存到文件中
  • “下拉刷新”更新列表
  • 无限滚动加载数据
  • 显示空状态 empty states

设置表格

xibstoryboard 中指定当前控制器的 IBOutlet

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 }

无限滚动分页

添加无限滚动支持

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

将以下内容添加到Podfile

pod 'FrameOk'

Carthage

将以下内容添加到Cartfile

github "MobileUpLLC/FrameOk"

手动

将原始文件夹中的文件拖拽到Xcode项目中

许可

FrameOk遵循MIT许可协议