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 进行授权,则可以在 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
设置表格
在 xib 或 storyboard 中指定当前控制器的 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许可协议。