VISPER
VISPER 是一个基于组件的库,它帮助您根据 VIPER 模式开发模块化应用程序。VISPER 包含几个组件,以在不过度浪费时间的条件下创建灵活的架构。
以下图片展示了使用 VISPER 编建的一个典型应用程序的架构。
嗯,我们知道这看起来有点复杂。
我们专门写了一篇文章来深入探讨这个问题。您可以在这里找到。
了解 VISPER 如何帮助您构建应用程序的最简单方法,是查看它的主要组件。
最通用的组件是 App 协议,它帮助您将应用程序由名为 Features 的独立模块组合而成。每个 Feature 都创建并配置应用程序的一个独特的部分。
由于 ViewControllers 的展示、创建和管理是您应用程序的重要部分,因此应该有一个单独的组件负责 ViewControllers 的展示和生命周期。这个任务由 Wireframe 负责。
Wireframe 允许您通过一个简单的 URL(称为 Route)路由到特定的 ViewController。它将 ViewController 的创建(由 ViewFeature 完成)与其展示(由名为 RoutingPresenter 的一些奇异的家伙完成)分开。
因为我们与庞大的 ViewController 作战了太多,我们希望我们的视图相当愚蠢。这为 Presenter 准备了舞台。在展示 ViewController 之前,Wireframe 会从 PresenterFeature 获取所有负责的 Presenters。它给了他们机会在真正展示之前,通过一些数据绑定和行为让它变得加智能。
控制的下一个重大挑战是管理您的应用程序状态。VISPER 决定在交互层中处理好这个问题,并支持您使用 Redux 架构 来管理您的应用程序状态和状态转换。
您可以通过向 ActionDispatcher 提交一个 Action 来在 presenter 中触发状态改变,并在另一边观察状态改变以更新您的视图。更多这方面的内容可以在 这里 finding。
索引
入门
由于VISPER由几个可以单独使用的独立组件组成,让我们从简单的WireframeApp开始,以了解您的ViewControllers的路由和生命周期是如何工作的。
在接下来的步骤中,我们将通过一些Redux内容扩展这个例子。
使用线框入门
从创建一个使用导航控制器(.navigationController)的简单项目开始。
内幕:VISPER可以管理您选择的任何容器视图控制器,但由于导航控制器在默认实现中预先配置,所以我们可以从它开始。
现在在您的AppDelegate中创建一个WireframeApp
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var visperApp: WireframeApp!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
let factory = DefaultWireframeAppFactory()
let visperApp = factory.makeApp()
self.visperApp = visperApp
// the rest of your implementation will follow soon ;)
}
默认WireframeAppFactory创建了一个已经配置好的WireframeApp,用于与导航控制器一起使用。如果您想了解更多关于线框的信息,您可以在这里找到这些信息,但现在让我们先完成WireframeApp的创建。
在下一步中,您需要告诉您的WireframeApp应该使用哪个导航控制器进行导航和路由。我们只需要一行来完成这个...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//just imagine we created your visperApp here
let navigationController = UINavigationController()
window.rootViewController = navigationController
//tell visper to work with this UINavigationController
visperApp.navigateOn(navigationController)
}
正如您所猜想的,我们的导航控制器在哪里完全无关紧要,它也可以放在UITabbarController或UISplitViewController中。VISPER将仅使用通过visperApp.navigateOn(controller)方法给出的最后一个导航控制器。
它不会为您保留它!所以如果它变得未保留,它就会消失,VISPER将抱怨不知道要使用哪个导航控制器!
您的WireframeApp现在已被初始化,并且渴望进行路由,但它没有要创建的ViewController,所以我们来创建一个ViewFeature来创建一些操作...
控制器可以是POVC(普通视图控制器
class StartViewController: UIViewController {
typealias ButtonTap = (_ sender: UIButton) -> Void
weak var button: UIButton! {
didSet {
self.button?.setTitle(self.buttonTitle, for: .normal)
}
}
var buttonTitle: String? {
didSet {
self.button?.setTitle(self.buttonTitle, for: .normal)
}
}
var tapEvent: ButtonTap?
override func loadView() {
let view = UIView()
self.view = view
let button = UIButton()
self.button = button
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(self.tapped(sender:)), for: .touchUpInside)
self.view.addSubview(button)
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: self.view.topAnchor),
button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
button.leftAnchor.constraint(equalTo: self.view.leftAnchor),
button.rightAnchor.constraint(equalTo: self.view.rightAnchor)
])
}
@objc func tapped(sender: UIButton) {
self.tapEvent?(sender)
}
}
别抱怨了,这个控制器真的很愚蠢,即使是不好看...但有时候你需要的只是一个能带你四处看看而且愿意帮忙的人...
所以我们就用他,把他放到一个ViewFeature里面。
剧透:ViewFeature是"VISPER_Wireframe"-Pod中的一个协议,必须实现它才能向frame提供ViewController。我们在VISPER-Pod中定义了一些typealias来提供"import VISPER"语句。这是一个有点脏的伎俩,但它允许你即使在我们的子Pod中,也能在VISPER的每个类或协议中使用"import VISPER"语句。
import VISPER
class StartFeature: ViewFeature {
let routePattern: String
// you can ignore the priority for the moment
// it is sometimes needed when you want to "override"
// an already added Feature
let priority: Int = 0
init(routePattern: String){
self.routePattern = routePattern
}
// create a blue controller which will be created when the "blue" route is called
func makeController(routeResult: RouteResult) throws -> UIViewController {
let controller = StartViewController()
controller.buttonTitle = "Hello unknown User!"
return controller
}
// return a routing option to show how this controller should be presented
func makeOption(routeResult: RouteResult) -> RoutingOption {
return DefaultRoutingOptionPush()
}
}
正如你所见,我们必须实现两个方法和一个属性,以便向frame提供ViewController。
我们来谈谈routePattern。它是一个描述用于匹配此控制器的路由的String。
这意味着routePattern var routePattern: String = "/start"
将匹配url的地址var url = URL(string: "/start")
,开始的容易但结尾的复杂...routePatterns可以包含变量、通配符和其他东西(有关更多信息,请参阅RoutePattern部分),现在我们假设我们对它们的工作方式有一个完整的理解。
下一个方法相当直接,makeController(routeResult: RouteResult) -> UIViewController
函数应该只是创建在wireframe.route(url: URL('/start')!)
被我们的应用中的任何地方调用时要展示的viewController(假设我们的routePattern是"/start")。
它的routeResult-参数包含一些可能在对ViewController创建时有用的上下文信息,尽管我们在这里不需要它(稍后查看RouteResult部分)
makeOption(routeResult:)
稍微复杂一些(如果需要查看RoutingOption部分)。它定义了frame默认如何展示ViewController。返回一个DefaultRoutingOptionPush
将导致frame只把我们的ViewController推向它当前的NavigationController。
好吧,这很简单
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//just imagine we created and initialized your visperApp here
let startFeature = StartFeature(routePattern: "/start")
try! visperApp.add(feature: startFeature)
}
visperApp现在将为我们注册的"/start"路由功能,这使我们能够用另一行代码将其路由到。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//just imagine we created and initialized your visperApp here
let startFeature = StartFeature(routePattern: "/start")
try! visperApp.add(feature: startFeature)
try! visperApp.wireframe.route(url: URL(string: "/start")!)
self.window?.makeKeyAndVisible()
}
你的AppDelegate的完整代码现在应该是这样的
import UIKit
import VISPER
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var visperApp: WireframeApp!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
let factory = DefaultWireframeAppFactory()
let visperApp = factory.makeApp()
self.visperApp = visperApp
let navigationController = UINavigationController()
window.rootViewController = navigationController
visperApp.navigateOn(navigationController)
let startFeature = StartFeature(routePattern: "/start")
try! visperApp.add(feature: startFeature)
try! visperApp.wireframe.route(url: URL(string: "/start")!)
self.window?.makeKeyAndVisible()
return true
}
}
如果你启动你的应用...“震撼弹”...你会发现一个绝对无用的丑陋黑色ViewController,中心有一个UIButton。但这是好事
添加一个展示者
管理和维护应用逻辑需要一个聪明的人,他知道他的环境,这显然不是愚蠢的ViewController,他无法处理除了视图之外的任何东西...
我们需要的人是能为愚蠢的视图连接到我们的交互层和frame层的单点技巧马饰演者。我们可以通过展示者来实现这一点。
展示者协议相当简单
protocol Presenter {
func isResponsible(routeResult: RouteResult, controller: UIViewController) -> Bool
func addPresentationLogic( routeResult: RouteResult, controller: UIViewController) throws
}
所以让我们创建一个可以配置我们的控制器的展示者。
class StartPresenter: Presenter {
var userName: String
init(userName: String) {
self.userName = userName
}
func isResponsible(routeResult: RouteResult, controller: UIViewController) -> Bool {
return controller is StartViewController
}
func addPresentationLogic(routeResult: RouteResult, controller: UIViewController) throws {
guard let controller = controller as? StartViewController else {
fatalError("needs a StartViewController")
}
controller.buttonTitle = "Hello \(self.userName)"
controller.tapEvent = { _ in
print("Nice to meet you \(self.userName)!")
}
}
}
展示者负责StartViewController类的ViewController,向按钮标题添加一些信息,并向按钮的点击事件添加一些行为。
让我们假定我们的演示者从其环境获取用户信息的依赖项,稍后我们在交互器/Redux 层添加该信息。
要将演示者添加到我们的应用中,我们只需将新的协议 PresenterFeature
扩展到我们的 StartFeature
上。
extension StartFeature: PresenterFeature {
func makePresenters(routeResult: RouteResult, controller: UIViewController) throws -> [Presenter] {
return [StartPresenter(userName: "unknown guy")]
}
}
然后从我们的 StartFeature
中删除以下令人讨厌的行(它应该创建组件,添加数据是演示者的工作)
func makeController(routeResult: RouteResult) throws -> UIViewController {
let controller = StartViewController()
// remove the following line, our feature shouldn't care about usernames ...
// controller.buttonTitle = "Hello unknown User!"
return controller
}
现在点击视图按钮将导致调试输出“很高兴认识你,陌生人!”但是,如果能够用一个带此消息的警告信息来展示那就更好了?
接受挑战?
让我们从创建一个新的功能开始,创建一个 UIAlertController
以展示...
class MessageFeature: ViewFeature {
var routePattern: String = "/message/:username"
var priority: Int = 0
func makeController(routeResult: RouteResult) throws -> UIViewController {
let username: String = routeResult.parameters["username"] as? String ?? "unknown"
let alert = UIAlertController(title: nil,
message: "Nice to meet you \(username.removingPercentEncoding!)",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Goodbye", style: .cancel, handler: nil))
return alert
}
func makeOption(routeResult: RouteResult) -> RoutingOption {
return DefaultRoutingOptionModal(animated: true,
presentationStyle: .popover)
}
}
这里发生了很多事情,所以让我们看看我们所做的...
首先看看路由模式 /message/:username
,它不是一个常见的匹配字符串。:username
定义了一个名为 'username'
的路由参数。这个路由将与任何以 /message/
开头的 URL 匹配,第二部分将被解释为路由参数 username
。如果您的路由 URL 为 URL(string: '/message/Max%20Mustermann')
,则路由参数 'username'
将为 'Max Mustermann'
。
其次,makeController(routeResult:)
函数从它的 RoutingResult 中提取了路由参数 'username'
。
最后,makeOption(routeResult:)
函数创建了一个 DefaultRoutingOptionModal
,这导致视图框架以 .popover
风格的模态视图控制器展示警告。
完成所有这些后,我们现在可以在 AppDelegate
中向我们的 WireframeApp
添加新功能。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//imagine visperApp is already initialized
let messageFeature = MessageFeature()
try! visperApp.add(feature: messageFeature)
//followed by other stuff
}
然后修改我们的 StartPresenter
以使用 Wireframe
来路由到消息功能。
class StartPresenter: Presenter {
var userName: String
let wireframe: Wireframe
init(userName: String, wireframe: Wireframe) {
self.userName = userName
self.wireframe = wireframe
}
func addPresentationLogic(routeResult: RouteResult, controller: UIViewController) throws {
guard let controller = controller as? StartViewController else {
fatalError("needs a StartViewController")
}
controller.buttonTitle = "Hello \(self.userName)"
controller.tapEvent = { [weak self] (_) in
guard let presenter = self else { return }
let path = "/message/\(presenter.userName)".addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
let url = URL(string:path)
try! presenter.wireframe.route(url: url!)
}
}
}
现有的内容在 tapEvent
闭包中处理。演示者创建一个 URL(string:"/message/unknown%20guy")
并告诉视图框架路由到这个 URL(如果我们假设用户名仍然是 "unknown guy"
)。
视图框架将 URL 匹配到我们的 MessageFeature
的路由模式,请求创建 ViewController,并通过解释 MessageFeature
给出的 RoutingOption
来展示它。
好了,一个问候我们的警告视图就显示出来了。
对于这样一个 AlertViewController,有相当多的事情在发生。但是这对于更大的 ViewController 来说扩展得相当好,帮助您保持它们简单、分离,并具有干净的架构,将展示和领域逻辑从 ViewController 中移除,放在它的位置上。
您还记得我们的 StartPresenter 并不知道从哪里获取用户名吗?这个问题的原因是这个示例不包括真实的交互器层。我们将通过添加一个 Redux 架构来解决此问题,在下一章中呈现。
您可以在VISPER.xcworkspace的Wireframe-Example中找到这个教程的代码。
添加 Redux
剧透:如果您在上一章没有完成,您可以使用 VISPER.xcworkspace 中的 Wireframe-Example 作为本教程的起点。您必须用以下代码替换此工作区中的 "Wireframe-Example" 部分,以进行上述操作。
target 'VISPER-Wireframe-Example' do pod 'VISPER-Wireframe', :path => '../' pod 'VISPER-Core', :path => '../' pod 'VISPER-UIViewController', :path => '../' pod 'VISPER-Objc', :path => '../' pod 'VISPER', :path => '../' end之后运行
pod install
。
如果您对 Redux 是一个全新的概念,请从阅读以下两篇文章开始。
如果您已经了解 Redux 但没有完全理解它的所有细节,请考虑阅读它们。
阅读完这些后,简要回顾最后一章的项目,并创建两个状态。
struct UserState: Equatable {
let userName: String
}
struct AppState: Equatable {
let userState: UserState?
}
剧透:使您的 AppState 符合 Equatable 是一个非必需但很好的实践,因为它使得处理状态变更变得更加容易。
AppState 应该包含我们应用程序的完整状态。由于整个应用程序的状态可能会变得相当复杂,最好是将其组成不同的子状态,以更本地化的方式处理状态复杂性。
虽然我们的状态并不真正复杂,但以这种方式开始是一个好主意,以演示如果需要,如何进行状态组合。
下一步应该是用 AnyVISPERAppRedux
对象来管理我们的状态变更。
从在 AppDelegate 中创建一个新方法开始
func createApp() -> AnyVISPERApp<AppState> {
fatalError("not implemented")
}
然后创建一个初始状态,当应用程序加载时使用
let initalState: AppState = AppState(userState: UserState(userName: "unknown stranger"))
现在,是时候编写一些样板代码,并编写 AppReducer,这是一个负责组合和减少 AppState 的函数。
let appReducer: AppReducer<AppState> = { (reducerProvider: ReducerProvider, action: Action, state: AppState) -> AppState in
let newState = AppState(userState: reducerProvider.reduce(action: action, state: state.userState))
return reducerProvider.reduce(action: action, state: newState)
}
它的结构始终相同,它创建一个新的 AppState,并将其与 reducerProvider 参数中的所有子状态进行减少。然后,它使用 reducer provider 减少新创建的状态本身,并返回结果。
剧透:我们很乐意为您这样做,但由于组合的 AppState 是一个泛型结构体,而无法动态创建这样的结构体,所以我们陷入了困境。我们最好的猜测是用 Sourcery 生成 AppReducer,您可以在 这里 找到如何做到这一点的教程。
现在创建一个 VISPERAppFactory
,创建您的应用程序并返回它。完整的 createApp()
函数现在应该是这样。
func createApp() -> AnyVISPERApp<AppState> {
let initalState: AppState = AppState(userState: UserState(userName: "unknown stranger"))
let appReducer: AppReducer<AppState> = { (reducerProvider: ReducerProvider, action: Action, state: AppState) -> AppState in
let newState = AppState(userState: reducerProvider.reduce(action: action, state: state.userState))
return reducerProvider.reduce(action: action, state: newState)
}
let appFactory = VISPERAppFactory<AppState>()
return appFactory.makeApplication(initialState: initalState, appReducer: appReducer)
}
下一步是将 visperApp 属性的类型更改为 AnyVISPERApp,并用您新创建的 createApp()
函数进行初始化。
您的 AppDelegate 现在应该是这样的
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var visperApp: AnyVISPERApp<AppState>!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
self.visperApp = self.createApp()
//imagine you initialized your features here
self.window?.makeKeyAndVisible()
return true
}
func createApp() -> AnyVISPERApp<AppState> {
let initalState: AppState = AppState(userState: UserState(userName: "unknown stranger"))
let appReducer: AppReducer<AppState> = { (reducerProvider: ReducerProvider, action: Action, state: AppState) -> AppState in
let newState = AppState(userState: reducerProvider.reduce(action: action, state: state.userState))
return reducerProvider.reduce(action: action, state: newState)
}
let appFactory = VISPERAppFactory<AppState>()
return appFactory.makeApplication(initialState: initalState, appReducer: appReducer)
}
}
哇,这些都是要使其运行所需的大量工作...
现在正在变得更好
现在是时候让你和我们的 StartPresenter
高兴起来,给他提供对状态访问权限。
我们首先通过将它的 userName
属性类型从 String
更改为 ObservableProperty
来开始这个过程。
ObservableProperty 是一个 ValueWrapper,它可以在其值更改时通知您。
class StartPresenter: Presenter {
var userName: ObservableProperty<String>
let wireframe: Wireframe
var referenceBag = SubscriptionReferenceBag()
init(userName: ObservableProperty<String?>, wireframe: Wireframe) {
//map our AppState ObservableProperty from String? to String
self.userName = userName.map({ (name) -> String in
guard let name = name, name.count > 0 else { return "unknown person"}
return name
})
self.wireframe = wireframe
}
// you already know the rest :P of it's implementation
...
}
如您所注意到的,我们添加了一个类型为 SubscriptionReferenceBag 的另一个属性。我们将使用它来订阅属性并将该订阅存储在演示者存在的时间(应该与演示者的控制器存在时间一样长)。如果您想了解更多关于此内容,请查看 SubscriptionReferenceBag 部分。
func addPresentationLogic(routeResult: RouteResult, controller: UIViewController) throws {
guard let controller = controller as? StartViewController else {
fatalError("needs a StartViewController")
}
let subscription = self.userName.subscribe { (value) in
controller.buttonTitle = "Hello \(value)"
}
self.referenceBag.addReference(reference: subscription)
controller.tapEvent = { [weak self] (_) in
guard let presenter = self else { return }
let path = "/message/\(presenter.userName.value)".addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
let url = URL(string:path)
try! presenter.wireframe.route(url: url!)
}
}
结果,当用户名更改时,按钮标题将更改。由于我们在 tapEvent
中的 url 路径从 presenter.userName
更改为 presenter.userName.value
,即使 MessageFeature 也表现得已经正确并使用我们 AppState 中的名称。
当您尝试构建项目时,您将收到一些错误,因为 StartPresenter
的构造函数已更改。它想要现在有一个 `ObservableProperty
让我们通过将其注入 StartFeature
和 StartPresenter
来改变这一点。
class StartFeature: ViewFeature {
let routePattern: String
let priority: Int = 0
let wireframe: Wireframe
let userName: ObservableProperty<String>
init(routePattern: String, wireframe: Wireframe,userName: ObservableProperty<String>){
self.routePattern = routePattern
self.wireframe = wireframe
self.userName = userName
}
// imagine the ViewFeature functions here
}
extension StartFeature: PresenterFeature {
func makePresenters(routeResult: RouteResult, controller: UIViewController) throws -> [Presenter] {
return [StartPresenter(userName: self.userName, wireframe: self.wireframe)]
}
}
我们将在 AppDelegate 中使用 map 将其注入到 StartFeature
中。
let startFeature = StartFeature(routePattern: "/start",
wireframe: visperApp.wireframe,
userName: visperApp.redux.store.observableState.map({ return $0.userState.userName ?? "Unknown Person"}))
现在构建应用程序会导致一个运行的应用程序,使用状态作为其响应式数据源。
class StartViewController: UIViewController, UITextFieldDelegate {
typealias ButtonTap = (_ sender: UIButton) -> Void
typealias NameChanged = (_ sender: UITextField, _ username: String?) -> Void
weak var button: UIButton! {
didSet {
self.button?.setTitle(self.buttonTitle, for: .normal)
}
}
weak var nameField: UITextField?
var buttonTitle: String? {
didSet {
self.button?.setTitle(self.buttonTitle, for: .normal)
}
}
var tapEvent: ButtonTap?
var nameChanged: NameChanged?
override func loadView() {
let view = UIView()
self.view = view
let nameField = UITextField(frame: .null)
nameField.translatesAutoresizingMaskIntoConstraints = false
self.nameField = nameField
self.navigationItem.titleView = nameField
nameField.placeholder = "enter your username here"
nameField.addTarget(self, action: #selector(textFieldChanged), for: .editingChanged)
nameField.backgroundColor = .white
let button = UIButton()
self.button = button
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(self.tapped(sender:)), for: .touchUpInside)
self.view.addSubview(button)
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: self.view.topAnchor),
button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
button.leftAnchor.constraint(equalTo: self.view.leftAnchor),
button.rightAnchor.constraint(equalTo: self.view.rightAnchor)
])
}
@objc func tapped(sender: UIButton) {
self.tapEvent?(sender)
}
@objc func textFieldChanged(textField: UITextField) {
self.nameChanged?(textField, textField.text)
}
}
class StartPresenter: Presenter {
//
// imagine some old properties here ...
//
let actionDipatcher: ActionDispatcher
init(userName: ObservableProperty<String?>,
wireframe: Wireframe,
actionDipatcher: ActionDispatcher) {
//use map to translate from ObservableProperty<String?> to ObservableProperty<String>
self.userName = userName.map({ (name) -> String in
guard let name = name, name.count > 0 else { return "unknown person"}
return name
})
self.wireframe = wireframe
//this one is new
self.actionDipatcher = actionDipatcher
}
//you already know the rest of our StartPresenter
}
class StartFeature: ViewFeature {
//
// imagine some old properties here ...
//
let actionDispatcher: ActionDispatcher
init(routePattern: String,
wireframe: Wireframe,
actionDispatcher: ActionDispatcher,
userName: ObservableProperty<String?>){
self.routePattern = routePattern
self.wireframe = wireframe
self.userName = userName
self.actionDispatcher = actionDispatcher
}
//
// and the rest of it's implementation following here ...
//
}
extension StartFeature: PresenterFeature {
func makePresenters(routeResult: RouteResult, controller: UIViewController) throws -> [Presenter] {
return [StartPresenter(userName: self.userName,
wireframe: self.wireframe,
actionDipatcher: self.actionDispatcher)]
}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//
// some intialization before ...
//
let startFeature = StartFeature(routePattern: "/start",
wireframe: visperApp.wireframe,
actionDispatcher: visperApp.redux.actionDispatcher,
userName: visperApp.redux.store.observableState.map({ return $0.userState.userName }))
try! visperApp.add(feature: startFeature)
//
// some routing afterwards ...
//
}
struct ChangeUserNameAction: Action {
let username: String?
}
func addPresentationLogic(routeResult: RouteResult, controller: UIViewController) throws {
guard let controller = controller as? StartViewController else {
fatalError("needs a StartViewController")
}
//ignore the whole view observing stuff normally done here
controller.nameChanged = { [weak self](_, text) in
self?.actionDipatcher.dispatch(ChangeUserNameAction(username: text))
}
}
struct ChangeUserNameReducer: ActionReducerType {
typealias ReducerStateType = UserState
typealias ReducerActionType = ChangeUserNameAction
func reduce(provider: ReducerProvider, action: ChangeUserNameAction, state: UserState) -> UserState {
return UserState(userName: action.username)
}
}
extension StartFeature: LogicFeature {
func injectReducers(container: ReducerContainer) {
container.addReducer(reducer: ChangeUserNameReducer())
}
}
public protocol App {
/// Add a feature to your application
///
/// - Parameter feature: your feature
/// - Throws: throws errors thrown by your feature observers
///
/// - note: A Feature is an empty protocol representing a distinct funtionality of your application.
/// It will be provided to all FeatureObservers after addition to configure and connect it to
/// your application and your remaining features. Have look at LogicFeature and LogicFeatureObserver for an example.
func add(feature: Feature) throws
/// Add an observer to configure your application after adding a feature.
/// Have look at LogicFeature and LogicFeatureObserver for an example.
///
/// - Parameter featureObserver: an object observing feature addition
func add(featureObserver: FeatureObserver)
}
功能和功能观察者
每当添加一个功能时,功能观察者就会被调用,其负责配置您的 VISPER 组件以提供您功能的实现。
许多 VISPER 组件实现了自己特有的App、功能和功能观察者子类型。
VISPER-Wireframe 提供了以下功能:
- WireframeApp
- ViewFeature
- PresenterFeature
- WireframeFeatureObserver
- ViewFeatureObserver
- PresenterFeatureObserver.
VISPER-Redux 提供以下功能:
VISPER-Swift 提供了一个VISPERApp,它结合了一个WireframeApp和一个ReduxApp的所有特性,并且是使用 VISPER-Framework 构建的最多的应用程序。
Wireframe
Wireframe 管理了 VISPER-Application 中 UIViewController 的生命周期。它用于创建控制器并从一位控制器路由到另一位控制器。它将 ViewController 的表现和创建逻辑从 UIViewController 本身分离出来。
Wireframe 组件包含了对 Wireframe 协议的实现。
您可以使用 DefaultWireframeAppFactory 创建一个带有默认配置的 WireframeApp。
let navigationController = UINavigationController()
let factory = DefaultWireframeAppFactory()
let wireframeApp = factory.makeApp()
wireframeApp.navigateOn(navigationController)
如果您 muốn创建一个不创建 WireframeApp 的 Wireframe,请使用 WireframeFactory。
let factory = WireframeFactory()
let wireframe = factory.makeWireframe()
现在创建一个 ViewFeature,它提供 ViewController 和一些 RoutingOptions,以定义控制器在你的线框中将如何展示。
class ExampleViewFeature: ViewFeature {
var routePattern: String = "/exampleView"
var priority: Int = 0
//controller will be pushed on current active navigation controller
func makeOption(routeResult: RouteResult) -> RoutingOption {
return DefaultRoutingOptionPush()
}
func makeController(routeResult: RouteResult) throws -> UIViewController {
let controller = UIViewController()
return controller
}
}
将其添加到您的 WireframeApp
let feature = ExampleFeature()
wireframeApp.add(feature: feature)
或添加到您的 Wireframe
let feature = ExampleFeature()
wireframe.add(controllerProvider: feature, priority: feature.priority)
wireframe.add(optionProvider: feature, priority: feature.priority)
try wireframe.add(routePattern: feature.routePattern)
您现在可以路由到 ExampleFeature 提供的控制器了
try wireframe.route(url: URL(string: "/exampleView")!)
路由模式
需要文档,请查看 JLRoutes 中路由模式的定义(我们早些时候借鉴了这个想法)。
VISPER-Redux
VISPER-Redux 是 swift 中对 redux 架构的一种实现。
它为您的应用程序架构提供了解决分布式应用程序状态和状态变化问题的方法。它是许多基于 VISPER 框架的应用程序中交互层的一种实现。
如果您想了解更多关于 redux 的信息,请查看以下教程和文档
有关 VISPER-Redux 的全面介绍可以在 这里 找到。
状态
VISPER-Redux 将您的应用程序的完整状态存储在一个中央结构中,以创建一个关于您不同应用程序组件当前状态的可透明表示。
一个典型的高级应用程序状态可能如下,用于管理下周的任务
struct AppState {
var userState: UserState
var todoListState: TodoListState
var imprintState: ImprintState
}
具有一些复合子状态
struct UserState {
var userName: String
var isAuthenticated: Bool
}
struct TodoListState {
var todos: [Todo]
}
struct ImprintState {
var text: String
var didAlreadyRead: Bool
}
应用归约器
每个商店都有一个特殊的高度器,其定义如下
public typealias AppReducer<State> = (_ ReducerProvider: ReducerProvider,_ action: Action, _ state: State) -> State
它作为对商店的单一入口点。只要分发一个动作,就会调用它来解决新的状态。由于我们的状态是通用的,必须将每个状态属性的创建委托给reducerProvider参数。
一个针对先前定义的AppState的AppReducer看起来应该这样
let appReducer = { (reducerProvider: ReducerProvider, action: Action, currentState: AppState) -> AppState in
let state = AppState(
userState: reducerProvider.reduce(action,currentState.userState),
todoListState: reducerProvider.reduce(action,currentState.todoListState),
imprintState : reducerProvider.reduce(action,currentState.imprintState)
)
return reducerProvider.reduce(action,state)
}
ReduxApp
创建一个redux应用很简单
let appState: AppState = AppState( ... create your state here ...)
let factory = ReduxAppFactory()
let app: ReduxApp<AppState> = factory.makeApplication(initialState: appState, appReducer: appReducer)
更改状态
在VISPER-Redux应用中,当前状态存储在一个中央Store实例中,该实例位于Redux类型的一个便捷包装对象中。仅通过在ActionDispatcher处发送一个动作(一个简单的消息对象)并在Reducer(一个reduce函数或一个类型为FunctionalReducer的实例)中创建一个修改后的新状态,才能实现状态更改。
reduce函数有如下形式(其中ActionType和StateType是Action和Any类型的泛型类型)
(_ provider: ReducerProvider,_ action: ActionType, _ state: StateType) -> StateType
该reduce函数/reducer将被应用于所有类型为ActionType的动作和所有类型为StateType的状态。您可以通过将其添加到reducer容器中来将reducer添加到您的redux架构中。
// add a reduce function
app.redux.reducerContainer.addReduceFunction(...)
// add a action reducer instance
app.redux.reducerContainer.addReducer(...)
一个动作只是一个简单的对象,符合空的Action协议,例如
struct SetUsernameAction: Action {
var newUsername: String
}
let action = SetUsernameAction(newUsername: "Max Mustermann")
app.redux.actionDispatcher.dispatch(action)
Reducer
Reducer指定了应用状态如何响应发送到商店的动作而更改。请记住,动作只描述已经发生的事情,但并不描述应用状态如何更改。在VISPER swift中的Reducer可以是reduce函数,或者一个类型为FunctionalReducer、ActionReducerType或AsyncActionReducerType的实例。
的实例。
ReduceFuntion
reduce函数只是一个简单的函数,它获取一个提供者、一个动作和一个状态,并返回相同类型的新状态。
let reduceFunction = { (provider: ReducerProvider, action: SetUsernameAction, state: UserState) -> UserState in
return UserState(userName: action.newUsername,
isAuthenticated: state.isAuthenticated)
}
reducerContainer.addReduceFunction(reduceFunction:reduceFunction)
FunctionalReducer
函数式reducer与reducer非常相似,只是reducer接受一个reduce函数并使用它来减少状态。
let functionalReducer = FunctionalReducer(reduceFunction: reduceFunction)
reducerContainer.addReducer(reducer:functionalReducer)
ActionReducerType
动作类型reducer是ActionReducerType类型的一个类,它包含特定动作和状态类型的reduce函数。
struct SetUsernameReducer: ActionReducerType {
typealias ReducerStateType = UserState
typealias ReducerActionType = SetUsernameAction
func reduce(provider: ReducerProvider,
action: SetUsernameAction,
state: UserState) -> UserState {
return UserState(userName: action.newUsername,
isAuthenticated: state.isAuthenticated)
}
}
let reducer = SetUsernameReducer()
reducerContainer.addReducer(reducer:reducer)
AsyncActionReducerType
异步reducer是AsyncActionReducerType类型的reducer,它不返回新的状态,而是调用一个带有新的状态的完成。
struct SetUsernameReducer: AsyncActionReducer {
typealias ReducerStateType = UserState
typealias ReducerActionType = SetUsernameAction
let currentState: ObserveableProperty<UserState>
func reduce(provider: ReducerProvider,
action: SetUsernameAction,
completion: @escaping (_ newState: UserState) -> Void) {
let newState = UserState(userName: action.newUsername,
isAuthenticated: self.currentState.value.isAuthenticated)
completion(newState)
}
}
let reducer = SetUsernameReducer(currentState: app.state.map{ $0.userState })
reducerContainer.addReducer(reducer:reducer)
LogicFeature
您可以使用LogicFeature向您的应用添加一些reducer。
import VISPER_Redux
class ExampleLogicFeature: LogicFeature {
func injectReducers(container: ReducerContainer) {
let reducer = SetUsernameReducer()
container.addReducer(reducer: incrementReducer)
}
}
let logicFeature = ExampleLogicFeature()
app.add(feature: let logicFeature)
Observing state change
观察状态变化很简单。只需观察您应用的状态即可。
//get your reference bag to retain subscription
let referenceBag: SubscriptionReferenceBag = self.referenceBag
//subscribe to the state
let subscription = app.state.subscribe { appState in
print("New username is:\(appState.userState.userName)")
}
//retain subscription in your reference bag
referenceBag.addReference(reference: subscription)
《VISPER-Redux》包含一个用于表示不断变化的AppState的ObservableProperty。ObservableProperty允许您订阅状态变化,并可映射到RxSwift-Observable。该功能在VISPER-Reactive组件中实现。
SubscriptionReferenceBag
需要记录文档
所有可用的VISPER组件的完整列表
- VISPER - 一个便捷的导入包装器,用于通过一个导入包含所有VISPER组件。它包含一些为向后兼容旧版本VISPER而弃用的组件。
- VISPER-Swift - VISPER框架的所有swift组件,以及它们所有依赖项的便捷导入包装器。
- VISPER-Objc - 将核心VISPER类包装起来,以便在objc代码库中使用它们。
- VISPER-Core - 某些常用的核心协议,用于在您的功能的不同组件之间进行通信。如果您想将VISPER组件集成到自己的项目和组件中,则应使用此pod。其协议在其他VISPER组件pod中实现。
- VISPER-Wireframe - 包含VIPER应用程序中wireframe层实现的组件,它管理ViewControllers的呈现和生命周期。
- VISPER-Presenter(swift / objc) - 包含VIPER应用程序中presentation层实现的组件。它包含一些presenter类,以将应用程序逻辑与视图逻辑分离。
- VISPER-Redux - 包含reducer架构实现的组件,许多VISPER应用程序使用它来表示Viper应用程序中的interactor层。
- VISPER-Reactive - 一个简单的响应式属性实现,允许在VISPER应用程序中使用响应式Redux架构。它可以由subspect VISPER-Rective/RxSwift更新,以使用RxSwift框架。
- VISPER-Sourcery - 一个组件,支持您通过为您创建一些必要的boilerplate代码来创建VISPER应用程序。
- VISPER-UIViewController (swift) / (objc) - 扩展UIViewControllers以通知presenter其生命周期(viewDidLoad等)的组件。
- VISPER-Entity - 如果在您的 VISPER-Application 中不使用自定义层,则用于建模实体层的组件。