VISPER
VISPER是一个基于组件的库,帮助您基于VIPER模式开发模块化应用程序。VISPER包含多个组件,以创建灵活的架构,而不会花费太多时间在VIPER上。
典型的应用程序,采用VISPER构建的架构,展示在下面的图像中。
好吧,我们知道这看起来有点复杂。
我们为此创建了单独的文章来深入研究这个主题。您可以在这里找到它。
了解VISPER如何帮助您构建应用程序,最快的方法是查看它的主要组件。
最一般的组件是App协议,它帮助您将应用程序组成称为功能的独立模块。每个功能都创建并配置应用程序的一个特定部分。
由于视图控制器的展示、创建和管理是您应用程序的重要部分,应该有一个单独的组件负责视图控制器的展示和生命周期。这项工作由骨架来完成。
骨架允许您通过简单的URL(称为路由)路由到特定的视图控制器。它将视图控制器的创建(由ViewFeature完成)与其展示(由一个名为RoutingPresenter的怪人完成)分离。
由于我们与大规模的视图控制器打了太多的仗,我们希望我们的视图尽可能傻。这为展示者做好了准备。在展示视图控制器之前,骨架从PresenterFeature请求所有负责的Presenters。它给了他们机会,在视图控制器实际展示前,用一些数据绑定和行为丰富这个愚蠢的东西。
下一个巨大挑战是控制应用程序的状态。VISPER决定在交互层中这样做,并支持您使用Redux架构来管理应用程序状态及其状态转换。
您可以通过提交一个行为到行为分发器来触发布局的更改,并在另一边观察更改状态来更新视图。有关此主题的更多信息,请参阅此处。
索引
入门
由于VISPER由几个可以单独使用的独立组件组成,我们不妨先从WireframeApp开始,以了解路由和ViewController的生命周期。
在下一步中,我们将使用一些Redux功能扩展这个示例。
从线框图入门
先创建一个简单的项目,该项目使用 UINavigationController。
剧透:VISPER可以管理您选择的任何容器ViewController,但由于默认实现中已经预配置了 UINavigationController,我们很容易从这里开始。
现在在您的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 ;)
}
DefaultWireframeAppFactory会创建一个已经配置好使用 UINavigationController 的 WireframeApp。如果您想了解更多有关线框图的信息,可以在此处找到该信息这里,但让我们先完成 WireframeApp 的创建。
在下一步中,您需要告诉您的 WireframeApp 应该使用哪个 UINavigationController 进行导航和路由。为此只需要一行代码...
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)
}
正如您所料,我们的 UINavigationController 的位置完全无关紧要,也可以将其放入 UITabbarController 或 UISplitViewController 中。VISPER将只使用 visperApp.navigateOn(controller) 方法给出的最后一位 UINavigationController。
它不会为您保留它!因此,如果它变成非保留的,它就会消失,并且VISPER会抱怨说它不知道应该使用哪个 UINavigationController!
您的 WireframeApp 现在已经初始化并准备好进行路由,但它还没有要创建的 ViewController,因此让我们创建一个 ViewFeature 来创建一些动作...
控制器可以是POVC(平凡旧ViewController
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 的一个协议,必须实现它以向 wireframe 提供 ViewController。我们在 VISPER-Pod 中定义了一些别名,以提供一个 "导入 VISPER" 语句。这招有点脏,但它允许你为每个 VISPER 类或协议使用 "导入 VISPER" 语句,即使它位于我们的子 pod 之一。
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()
}
}
如你所见,有两个方法和一个属性必须实现,以向 Wireframe 提供ViewController。
让我们从 routePattern 开始。它是一个描述用于匹配此控制器的路由的字符串。
这意味着 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")。
It's routeResult-Parameter contains some context information that might be useful when creating a ViewController, although we don't need it here (have a look at the RouteResult section later
makeOption(routeResult:)
是一个更复杂的方法(需要时请查看 RoutingOption 小节)。它定义了 wireframe 如何默认显示 ViewController。返回 DefaultRoutingOptionPush
将导致 wireframe 只要将我们的 ViewController 推送到它的当前 UINavigationController 中。
Easy
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,他处理不了除了视图之外的任何事情...
我们需要的是一个人,他能将那个愚蠢的视图与我们的 interactor 和 wireframe 层中的独奏马戏团连接起来。我们可以用 展示者 来做到这一点。
展示者协议相当简单。
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,向按钮标题添加一些信息,并为按钮的点击事件添加一些行为。
假设我们的演示者从其环境依赖中获取用户名的信息,我们将在interactor/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
}
点击视图按钮现在将产生调试输出"很高兴认识你,未知的人!",但这不是很想用这个信息显示一个提醒消息吗?
挑战接受?
让我们从创建一个新的Feature开始,创建一个显示...的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》,这使得wireframe以.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")》并告诉wireframe将其路由到此URL(如果我们假设username仍然为"unknown guy"
)。
Wireframe将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 应该包含我们应用的全状态。由于整个应用的状态可能相当复杂,最好是将其组合成不同的子状态,更本地化地解决问题。
虽然我们的状态并不真正复杂,但以这种方式开始是一个好主意,以展示如果需要,如何进行状态组合。
下一步是将我们的 WireframeApp 替换为 AnyVISPERApp<AppState>。这为我们提供了一个 Redux
对象来管理我们的状态变化。
在你的 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 提供者一起减小并返回结果。
剧透:我们很想为你这样做,但由于组合的 AppState 是一个通用的 struct,并且没有方法可以动态地创建这样的 struct,所以我们陷入了困境。我们最好的猜测是使用 Sourcery 生成 AppReducer,你可以在这里找到教程:[链接地址](https://docs/README-VISPER-Sourcery.md)。
现在创建一个 VISPERAppFactory<AppState>
,创建你的应用并返回它。现在完整的 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<String>
开始这个过程。
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 的属性。我们将使用它来订阅属性,并将订阅存储在 presenter 存在的整个时间(这应该与控制器存在的时间一样长)。如果你想了解更多信息,请检查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<String?>` 参数。
让我们通过将其注入到 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)]
}
}
我们将使用 map 在你的 AppDelegate 中将其注入到 StartFeature
。
let startFeature = StartFeature(routePattern: "/start",
wireframe: visperApp.wireframe,
userName: visperApp.redux.store.observableState.map({ return $0.userState.userName ?? "Unknown Person"}))
现在构建应用会使应用运行,使用 appstate 作为其响应式数据源。
如果你的状态不发生变化,有一个响应式数据源会很无聊。现在是时候引入一些状态改变了。
我们从给StartViewController添加一个输入字段开始。
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)
}
}
这里并没有什么魔法发生,它只是我们普通的UIViewController,其中包含一个按钮和在它的标题视图中有一个UITextField。现在,如果我们你在文本字段中输入用户名,我们想要更改我们的appstate。由于我们的视图只需要关注显示内容和响应用户输入,触发状态改变应该在presenter中完成。
presenter通过向ActionDispatcher(了解更多这里)分派一个Action(一个简单的消息对象)来触发状态改变。所以让我们给他提供这样一个东西。
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
}
由于我们现在需要一个ActionDispatcher来初始化presenter,让我们也把我们的StartFeature
给它。
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)]
}
}
并在AppDelegate中注入它。
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 ...
//
}
现在我们需要一个StartPresenter
要分发的Action。
struct ChangeUserNameAction: Action {
let username: String?
}
它只是一个非常简单的结构体,携带(有些人可能会说“传递”)我们的用户名字符串到更改应用状态的重减器。
在presenter中分发给它很容易。
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))
}
}
你可以构建你的应用程序了,但如果你输入你的用户名,应用程序的状态并没有改变。
那是什么鬼?
嗯,没有Reducer处理当接收到一个ChangeUserNameAction
时对UserState
(以及结果AppState
)的改变。
但还有希望,构建这样的结构很容易(它甚至可以被Sourcery生成)
struct ChangeUserNameReducer: ActionReducerType {
typealias ReducerStateType = UserState
typealias ReducerActionType = ChangeUserNameAction
func reduce(provider: ReducerProvider, action: ChangeUserNameAction, state: UserState) -> UserState {
return UserState(userName: action.username)
}
}
在你的StartFeature中添加后,就完成了。
extension StartFeature: LogicFeature {
func injectReducers(container: ReducerContainer) {
container.addReducer(reducer: ChangeUserNameReducer())
}
}
现在启动你的应用程序应该会导致应用程序在输入用户名时实时更改按钮的标题。
你可以在VISPER.xcworkspace中的VISPER-Swift-Example中找到这个教程的代码。
组件
应用
你VISPER应用程序的核心组件是App协议的一个实例,它允许你通过一个Feature来配置你的应用程序,该Feature代表了应用程序的一个独立功能,并配置了它所使用的所有VISPER组件。
声明App协议非常简单
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)
}
功能与功能观察者
基本添加一些FeatureObservers和Features到App中。
每当添加了Feature时,FeatureObserver将会被调用,负责配置你的VISPER组件,以提供你的Feature的功能。
许多VISPER组件实现了自己的App、Feature和FeatureObserver子类型。
VISPER-Wireframe为你提供:
- WireframeApp
- ViewFeature
- PresenterFeature
- WireframeFeatureObserver
- ViewFeatureObserver
- PresenterFeatureObserver.
VISPER-Redux为你提供:
VISPER-Swift提供了一个VISPERApp,它结合了WireframeApp和ReduxApp的所有特性,并在大多数使用VISPER-Framework构建的App中应用。
Wireframe
Wireframe管理VISPER-Application中UIViewController的生命周期。它用于创建控制器并将一个控制器路由到另一个控制器。它将ViewController的展示和创建逻辑与UIVC本身分离。
VISPER-Wireframe组件包含Wireframe-Protocol的实现。
你可以使用DefaultWireframeAppFactory创建一个具有默认配置的WireframeApp。
let navigationController = UINavigationController()
let factory = DefaultWireframeAppFactory()
let wireframeApp = factory.makeApp()
wireframeApp.navigateOn(navigationController)
如果你想创建一个不创建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")!)
Routepatterns
必须进行文档化,请查看JLRoutes中对RoutePatterns的定义(我们是从他们那里借鉴了这个想法)。
VISPER-Redux
VISPER-Redux 是 Swift 中 Redux 架构的一种实现。
它为你提供了一个应用架构来解决分布式应用状态和状态变化的问题。它是基于 VISPER 框架的许多应用中交互层实现。
如果你想了解更多关于 Redux 的内容,请参考以下教程和文档
对 VISPER-Redux 的综合介绍可在此找到。
State
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
}
AppReducer
每个存储都有一个专用减少器,如下定义
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(一个函数或类型为 FunctionalReducer 的实例)中创建一个新的修改过状态。
Reducer 函数具有以下形式(其中 ActionType 和 StateType 是 Action 和 Any 类型的泛型类型)
(_ provider: ReducerProvider,_ action: ActionType, _ state: StateType) -> StateType
Reducer 函数/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
Reducers指定了应用状态在向商店发送动作时如何更改。请记住,动作只描述了发生了什么,但并没有描述应用程序状态如何改变。VISPER swift中的Reducer可以是reduce函数,也可以是类型为 FunctionalReducer、ActionReducerType 或 AsyncActionReducerType 的实例。
或
ReduceFuntion
减少函数只是获取一个提供者、一个动作和一个状态,并返回同类型的新状态的一个简单函数。
let reduceFunction = { (provider: ReducerProvider, action: SetUsernameAction, state: UserState) -> UserState in
return UserState(userName: action.newUsername,
isAuthenticated: state.isAuthenticated)
}
reducerContainer.addReduceFunction(reduceFunction:reduceFunction)
FunctionalReducer
函数式减少函数与减少函数非常相似,只是一个减少函数接受一个减少函数并将它用于减少状态。
let functionalReducer = FunctionalReducer(reduceFunction: reduceFunction)
reducerContainer.addReducer(reducer:functionalReducer)
ActionReducerType
动作类型减少函数是一个包含特定动作和状态类型的减少函数的 ActionReducerType 类。
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
异步减少函数是 AsyncActionReducerType 的一种,它不返回新状态,而是调用一个带有新状态的完成函数。
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 向您的应用程序添加一些减少函数。
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 包含一个 ObservableProperty 来表示变化的 AppState。 ObservableProperty 允许您订阅状态变化,并可映射到 RxSwift-Observable。这是在 VISPER-Reactive 组件中实现的。
SubscriptionReferenceBag
需要文档化
所有可用的VISPER组件的完整列表
- VISPER - 一个便利的导入包装器,用于通过一个导入包含所有VISPER组件。它包含一些过时的组件,以与以前的VISPER版本保持向后兼容。
- VISPER-Swift - VISPER框架的所有Swift组件,及其依赖项的便利导入包装器。
- VISPER-Objc - 一个包装在核心VISPER类中的包装器,用于在objective C代码库中使用它们。
- VISPER-Core - 一些通用的核心协议,用于在您特性的不同组件之间进行通信。如果您希望将VISPER组件包含到自己的项目中,应该使用此pod。它的协议在其他VISPER组件pods中实现。
- VISPER-Wireframe - 包含VIPER应用程序中wireframe层实现的组件,它管理ViewControllers的表示和生命周期。
- VISPER-Presenter(swift / objc) - 包含VIPER应用程序中presentation层实现的组件。它包含一些表示类,用于将应用程序逻辑与视图逻辑分开。
- VISPER-Redux - 包含在许多VIPER应用程序中使用的redux架构实现的组件,用于表示VIPER应用程序中的interactor层。
- VISPER-Reactive - 一个简单的响应性属性实现,允许在VISPER应用程序中使用响应式redux架构。它可以通过subspect VISPER-Rective/RxSwift更新,以使用RxSwift框架。
- VISPER-Sourcery - 支持您通过为您创建一些必需的样板代码来创建VISPER应用程序的组件。
- VISPER-UIViewController (swift) / (objc) - 扩展UIViewControllers以通知表示者其生命周期(viewDidLoad等)的组件。
- VISPER-Entity - 一个组件,用于在没有自定义层的情况下模拟VISPER应用程序的entity层。