VISPER
VISPER 是一个基于组件的库,它帮助您根据 VIPER 模式开发模块化应用程序。VISPER 包含了几个组件,可以在不花费太多时间的前提下创建一个灵活的架构。
使用 VISPER 构建的典型应用程序架构在下面的图片中显示。
好吧,……我们知道这看起来有点复杂。
我们为此主题创建了一篇单独的文章,您可以在这里找到它 here。
了解 VISPER 如何帮助您构建应用程序的最佳方法是通过查看它的主要组件。
最通用的组件是 App 协议,它帮助您用称为 功能 的独立模块组合您的应用程序。每个功能创建并配置您应用程序的独立部分。
由于视图控制器(ViewController)的呈现、创建和管理是您应用程序的一个重要部分,因此应该有一个专门的组件负责视图控制器的呈现和生命周期。这项工作由 Wireframe 负责。
Wireframe 允许您通过简单的 URL(称为 Route)路由到特定的 ViewController。它将 ViewController 的创建(由 ViewFeature 完成)与其呈现(由一些奇怪的名为 RoutingPresenter 的人完成)分开。
由于我们与庞大的 ViewController 作了太多的斗争,我们希望我们的视图相当简单。这为 Presenter 准备了舞台。Wireframe 在呈现 ViewController 之前,从 PresenterFeature 中请求所有负责的 Presenters。它允许他们在真正呈现之前通过一些数据绑定和行为来丰富这个愚蠢的东西。
下一个主要挑战是控制应用程序的状态。VISPER 决定在交互层中这样做,并支持您使用 Redux 架构 来管理您的应用程序状态及其状态转换。
您可以通过向 ActionDispatcher 提交一个 Action 在呈现器中触发状态更改,并在另一边观察状态更改以更新您的视图。有关此主题的更多信息,请参阅 here。
索引
入门
由于VISPER由几个可以单独使用的独立组件组成,让我们从小处着手,仅创建一个WireframeApp,以了解您的ViewControllers的路由和生命周期。
在下一步中,我们将扩展此示例,添加一些Redux的内容。
开始使用线框图
从创建一个使用UINavigationController的项目开始。
提示:VISPER可以管理您的任何容器视图控制器,但由于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(普通视图控制器
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 中定义了一些别名类型,以便用 "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()
}
}
正如你可能看到的那样,有两个方法和一个属性必须实现,以向 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")。
routeResult 参数包含一些可能对创建 ViewController 有用的上下文信息,尽管我们在这里不需要它(稍后查看RouteResult部分)
makeOption(routeResult:)
稍微复杂一些(如果需要,请查看RoutingOption部分)。它定义了默认情况下 wireframe 如何显示 ViewController。返回 DefaultRoutingOptionPush
将导致 wireframe 只推送我们的 ViewController 到它当前的自定义导航控制器中。
简单
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,他将无法应对任何除了视图之外的事情……
我们需要的是一个人来连接愚蠢的视图和我们的交互和 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,向按钮标题添加一些信息,并添加一些按钮点击事件的行为。
让我们假设我们的表演者从它的环境中获得有关用户名称的信息作为依赖,我们将在交互器/Redux 层添加该信息。
要将表演者添加到我们的应用程序中,我们只需要扩展 StartFeature 并使用新的协议 PresenterFeature
。
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应该包含我们应用的全部状态。由于整个应用的状态可能相当复杂,最好是将其组合为不同的子状态,以更本地化的方式克服状态复杂性。
尽管我们的状态并不真的很复杂,但以这种方式开始是个好主意,以展示如果需要时如何进行状态组合。
下一步应该是在我们的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提供者将新创建的状态自身缩减,并返回结果。
剧透:我们很乐意为此编写程序,但由于组合的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属性的Type更改为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的属性。我们将使用它来订阅属性并将订阅保存,直到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
让我们通过向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中将它映射到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(位于标题视图中)。现在,如果我们你在文本字段中输入用户名,我们就要改变我们的应用状态。由于我们的视图只需要考虑查看数据和响应用户输入,状态改变的操作应该在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?
}
它只是一个非常简单的struct,携带(有些人会说是传递)着我们的用户名字符串到改变应用状态的reducers。
在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))
}
}
现在你可以构建你的app了,但如果你输入用户名,应用状态并没有改变。
那这是怎么回事?
嗯,在收到ChangeUserNameAction
时,并没有Reducer处理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())
}
}
现在启动应用应该会导致一个在输入用户名时改变按钮标题的响应式app
这个教程的代码可以在VISPER.xcworkspace中的VISPER-Swift-Example中找到}}
组件
应用
你VISPER应用的核心组件是一个App协议的实例,它通过一个表示你应用程序独特功能并配置所有使用的VISPER组件的Feature来配置你的应用程序。
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)
}
功能和FeatureObserver
你基本上可以向一个App添加一些FeatureObservers和Features。
每当有 FeatureObserver 被调用时,都会添加一个 Feature,并负责配置您的 VISPER 组件以提供由您的 Feature 实现的功能。
许多 VISPER 组件实现了自己的 App、Feature 和 FeatureObserver 子类型。
VISPER-Wireframe 为您提供了以下内容:
- WireframeApp
- ViewFeature
- PresenterFeature
- WireframeFeatureObserver
- ViewFeatureObserver
- PresenterFeatureObserver.
VISPER-Redux 为您提供以下内容:
VISPER-Swift 提供了一个 VISPERApp,它结合了 WireframeApp 和 ReduxApp 的所有特性,并在使用 VISPER-Framework 开发的多数 Apps 中使用。
Wireframe
Wireframe 管理了 VISPER-Application 中 UIViewController 的生命周期。它被用来创建控制器,并从控制器之间进行路由。它将 ViewController 的展示和创建逻辑与 UIViewController 本身分离。
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 和一些路由选项定义了如何展示控制器,将其添加到您的 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
}
AppReducer
每个存储都有一个特殊定义为以下内容的 reducer
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函数或类型为 FunctionalReducerActionReducerType 或 AsyncActionReducerType 的实例)中来实现。
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
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)
观察状态变化
观察状态变化很简单。只需观察您应用的状态。
//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类可以在objc代码库中使用。
- VISPER-Core - 用于在不同组件之间通信的一些常见核心协议,如果你想在自己的项目中包含VISPER组件和组件,则应使用此pod。它的协议在其他的VISPER组件pod中实现。
- VISPER-Wireframe - 包含实现在VIPER应用中的wireframe层的组件,它管理ViewController的呈现和生命周期。
- VISPER-Presenter(swift / objc) - 包含实现在VIPER应用中的presentation层的组件。它包含一些presenter类,用于分离你的应用逻辑和视图逻辑。
- VISPER-Redux - 包含在许多VISPER应用中实现的一种redux架构的组件,用于表示viper应用中的interactor层。
- VISPER-Reactive - 一个简单的reactive属性实现,允许在VISPER应用中使用reactive redux架构。可以通过subspec VISPER-Rective/RxSwift进行更新,以使用RxSwift框架。
- VISPER-Sourcery - 一个组件,支持你通过为你的应用创建一些必要的样板代码来创建VISPER应用。
- VISPER-UIViewController (swift / objc) - 扩展UIViewControllers的组件,以通知presenter其生命周期(viewDidLoad等。)
- VISPER-Entity - 一个组件,用于在你不使用自己的层的情况下建模VISPER应用中的entity层。