VISPER
VISPER 是一个基于组件的库,它帮助您根据 VIPER 模式开发模块化应用程序。VISPER 包含多个组件,以创建一个灵活的架构,同时不会花费太多时间在 VIPER 上。
以下是使用 VISPER 构建的典型应用程序结构图。
好吧 ... 我们知道这看起来有点复杂。
我们创建了一篇单独的文章来深入研究这个主题。您可以在这里找到它。
了解 VISPER 如何帮助您构建应用程序的最简单方法是查看它的主要组件。
最通用的组件是名为 App 的协议,它帮助您将应用程序组合成分离的模块,称为 功能。每个功能都创建并配置应用程序的一个独特部分。
由于呈现,创建和管理 ViewControllers 是您应用程序的一个重要部分,因此应该有一个单独的组件负责 ViewControllers 的呈现和生命周期。这项任务由 Wireframe 完成。
Wireframe 允许您通过一个简单的 URL(称为 Route)路由到一个特定的 ViewController。它将 ViewController 的创建(由 ViewFeature 完成)与其呈现(由一些名为 RoutingPresenter 的奇怪家伙完成)分开。
由于我们与庞大的 ViewController 斗争太多,我们希望我们的视图相当愚蠢。这为 Presenter 做了准备。Wireframe 在呈现 ViewController 之前,从 PresenterFeature 中请求所有负责的 Presenters。它给予他们向那个愚蠢的东西添加一些数据绑定和行为的机会,然后再呈现。
下一个重大挑战是控制应用程序的状态。VISPER 自己决定在交互层这样做,并支持您使用 Redux 架构来管理您的应用程序状态和状态转换。
您可以通过向 ActionDispatcher 提交 Action 来在 presenter 中触发状态更改,并在另一边观察状态更改以更新视图。有关更多信息,请参阅这里。
索引
入门
由于VISPER由几个独立的组件组成,这些组件可以单独使用,所以让我们从简单的WireframeApp开始,以便了解您ViewController的路由和生命周期。
在下一步中,我们将通过添加一些Redux内容来扩展此示例。
使用布局图入门
从创建一个使用getElementsByTagName的简单项目开始。
剧透:VISPER可以管理您选择的任何容器ViewController,但由于默认实现中预先配置了getElementsByTagName,所以从它开始很容易。
现在在您的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创建了一个已配置用于与getElementsByTagName一起使用的WireframeApp。如果您想了解更多关于布局图的信息,可以在这里找到这些信息,但让我们暂时完成WireframeApp的创建。
在下一步中,您必须告诉您的WireframeApp应使用哪个getElementsByTagName进行导航和路由。为此只需要一行代码...
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)
}
正如你所猜到的,我们的getElementsByTagName到底在哪里,也是无关紧要的,你也可以将它放在UITabbarController或UISplitViewController中。VISPER只会使用visperApp.navigateOn(controller)方法给出的最后一个getElementsByTagName。
它不会为您保留它!所以如果它变成了非保留的,它就会消失,VISPER会抱怨不知道该使用哪个getElementsByTagName!
您的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中定义了一些typealiases,以便提供"import VISPER"语句。这确实是一个小花招,但它允许你为每个VISPER类或协议使用"import 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开始。它是一个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")。
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中。
好了,这很简单
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层的独角兽。我们可以通过展示者来实现这一点。
Presenter-Protocol相当简单
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/
开头且第二部分被解释为路由参数 username
的 URL 都将匹配这个路由。如果你路由的 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 简单、分离,并使用干净的架构将展示和领域逻辑从 ViewController 中分离出来。
你还记得我们的 StartPresenter 真不知道应该从哪里检索用户名吗?这个问题的原因是这个示例没有包含真实的交互层。我们将在下一章向这个示例添加 Redux 架构来解决这个问题。
您可以在 VISPER.xcworkspace 中的 Wireframe-Example 中找到这个教程的代码。
添加 Redux
抢先看:如果您没有完成最后一章,可以从 VISPER.xcworkspace 中的 Wireframe-Example 代码作为此教程的起点。您必须在工作区的 Podfile 中的“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
在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是一个通用的struct且没有创建动态创建的struct的方法,所以我们在这里遇到了困境。我们最好的猜测是使用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
属性的Type更改为,并用您新创建的
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的属性。我们将使用它来订阅到该属性,并在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
中从presenter.userName
更改为presenter.userName.value
,即使MessageFeature也已经正确地使用我们应用程序的状态中的名称。
当您尝试构建项目时,您将遇到一些错误,因为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"}))
现在构建应用将导致运行中的应用程序使用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。现在,当您在文本字段中输入用户名时,我们要更改我们的app状态。由于我们的视图应该只关心查看内容和响应用户输入,因此触发状态变化应在presenter中完成。
通过向ActionDispatcher(可在此处了解更多信息)发送一个操作(一个简单的消息对象),presenter触发一个状态变化。所以让我们提供给他这样一个东西。
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
分发的操作。
struct ChangeUserNameAction: Action {
let username: String?
}
它只是一个非常简单的结构体,将我们的用户名字符串“携带”到改变应用程序状态的reducer。
在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)
}
功能与功能观察者
每当添加一个 特征观察器 时,将会被调用,并负责配置您的 VISPER 组件以提供由您的 功能 实现的功能。
许多 VISPER 组件实现了自己特有的 App、特征 和 特征观察器 类型。
VISPER-Wireframe 为您提供:
- WireframeApp
- ViewFeature
- PresenterFeature
- WireframeFeatureObserver
- ViewFeatureObserver
- PresenterFeatureObserver.
VISPER-Redux 为您提供:
VISPER-Swift 提供了一个 VISPERApp,它结合了 WireframeApp 和 ReduxApp 的所有特性,并广泛用于以 VISPER-Framework 构建的 App。
Wireframe
Wireframe 管理 VISPER-Application 中的 UIViewController 生命周期。它用于创建控制器和从一个控制器路由到另一个控制器。它将 ViewController 的表示和创建逻辑与 UIViewController 本身分离。
VISPER-Wireframe 组件包含 Wireframe 协议的实现。
您可以使用 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 和一些路由选项,以定义控制器如何被呈现,并作为对 wireframe 的提供。
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")!)
以下是一个使用 VISPER 和 wireframe 的完整示例
路由模式
需要提供文档,请查看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
}
应用程序Reducer
每个存储都有一个特殊的具有以下定义的reducer
public typealias AppReducer<State> = (_ ReducerProvider: ReducerProvider,_ action: Action, _ state: State) -> State
它被用作存储的单个入口点。每当发送一个action时,都会调用它以解决新的状态。由于我们的状态是通用的,因此需要将每个状态属性的创建委托给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,ActionReducerType 或 AsyncActionReducerType 的实例)中创建修改后的新状态。
reduce 函数具有以下形式(其中 ActionType 和 StateType 是类型为 Action 和 Any 的泛型类型)
(_ provider: ReducerProvider,_ action: ActionType, _ state: StateType) -> StateType
reduce 函数/Reducer 将应用于所有类型为 ActionType 的动作和类型为 StateType 的所有状态。可以通过将其添加到 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
功能还原器与之前类似,只是一个接受还原函数并使用它来简化状态的还原器。
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)
观察状态更改
观察状态更改很简单。只需观察您应用程序的状态即可。
//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-Components 列表
- VISPER - 一个方便的导入包装器,可以一次导入所有 VISPER 组件。它包含一些为与之前 VISPER 版本向后兼容而废弃的组件。
- VISPER-Swift - VISPER-Framework 中的所有 Swift 组件以及它们的依赖项的方便导入包装器。
- VISPER-Objc - 一个包装核心 VISPER 类的包装器,以便在 objc 代码库中使用。
- VISPER-Core - 用于在您的功能的不同组件之间通信的一些常见核心协议。如果您想将 VISPER 组件包含到自己的项目中,则应使用此 pod。它的协议在其他的 VISPER 组件 pods 中实现。
- VISPER-Wireframe - 包含在 VIPER-应用程序中实现 wireframe 层的组件,它管理观控制器(ViewController)的显示和生命周期。
- VISPER-Presenter(swift / objc) - 包含在 VIPER-应用程序中实现展示层的组件。它包含一些演讲者类以将应用程序逻辑与视图逻辑分离。
- VISPER-Redux - 包含在许多 VISPER-应用程序中用于表示 interactor 层的 Redux 架构的组件。
- VISPER-Reactive - 简单实现响应式属性,允许在 VISPER-应用程序中使用响应式 Redux 架构。它可以通过 subspec VISPER-Rective/RxSwift 进行更新以使用 RxSwift 框架。
- VISPER-Sourcery - 支持您通过为您创建一些必要的样板代码来创建 VISPER 应用程序的组件。
- VISPER-UIViewController (swift) / (objc) - 扩展 UIViewControllers 的组件,可向演讲者通知其生命周期(viewDidLoad 等)
- VISPER-Entity - 如果您在VISPER-Application中不使用自定义层,则用于建模实体层的一个组件。