VISPER
VISPER 是一个基于组件的库,它帮助您使用 VIPER 模式开发模块化应用。VISPER 包含几个组件来创建一个灵活的体系结构,而不会因使用 VIPER 而花费太多时间。
以下是使用 VISPER 构建的应用的典型结构的图像。
好了,我们知道了这看起来有点复杂。
我们为此主题创建了一篇单独的文章,您可以在这里找到它:这里。
了解 VISPER 如何帮助您构建应用的最佳方式是查看其主组件。
最一般的组件是 App 协议,它帮助您将您的应用组成多个模块,称为 特性。每个特性都会创建和配置应用程序的一个特定部分。
由于表示、创建和管理 ViewController 是您应用程序的一个重要部分,应该有一个单独的组件负责表示和 ViewController 的生命周期。这项工作由 Wireframe 完成。
Wireframe 允许您通过简单的 URL(称为 Route)路由到特定的 ViewController。它将 ViewController 的创建(由 ViewFeature 完成)与其表示(由名为 RoutingPresenter 的怪异人物完成)分开。
由于我们与庞大的 ViewController 作战过多,我们希望视图非常愚蠢。这为 Presenter 准备了舞台。Wireframe 在呈现 ViewController 之前,从 PresenterFeature 请求所有负责的 Presenters。它给他们提供了一些数据绑定和行为的机会,以便实际呈现之前丰富那个愚蠢的东西。
下一个重大挑战是控制应用程序的状态。VISPER 选择在 Interactor 层中执行此操作,并支持您使用 Redux 体系结构来管理应用程序状态及其状态转换。
您可以通过提交一个 操作 给 ActionDispatcher 来在Presenter中触发状态变化,然后观察状态变化以更新您的视图。更多关于此主题的信息,请参阅这里。
索引
入门
由于VISPER由几个独立的、可以独立使用的组件组成,我们可以先轻松地从一个WireframeApp开始,以了解路由和ViewController的工作生命周期。
在接下来的步骤中,我们将扩展这个示例,添加一些Redux内容。
从线框图开始入门
从创建一个使用UINavigationController的简单项目开始。
剧透:VISPER可以管理您选择的任何容器ViewControllers,但由于在默认实现中已经预配置了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。
它不会保留它!所以如果它变成了unretained,它就会消失,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中定义了一些typealiases以提供"导入VISPER"语句。这是一个肮脏的伎俩,但它允许你在我们的子Pod中的任何地方使用"导入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")。
routeResult参数包含一些上下文信息,这些信息在创建ViewController时可能很有用,尽管我们在这里不需要它(稍后查看RouteResult部分)
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,他无法处理除了视图之外的其他任何事情...
我们需要的是某人将愚蠢的视图与我们交互层和wireframe层中的单技能“独角兽”连接起来。我们用展示者(Presenter)来实现这一点。
展示者协议非常简单
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 层添加该信息。
要将演示者添加到我们的应用程序中,我们只需要将我们的 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(如果我们假设 username 仍然是 "unknown guy"
)。
线框将 URL 匹配到我们的 MessageFeature
的路由模式,要求它创建 ViewController,并通过解析 MessageFeature
给出的 RoutingOption
来呈现它。
好了!一个问候我们的警报视图出现了。
对于一个 AlertViewController 来说,发生了很多事情。但是对于更大的 ViewController 而言,它可以很好地扩展,有助于保持它们简单、分离,并拥有一个干净的架构,将呈现和领域逻辑从 ViewController 中分离出来,放在其适当的位置。
你还记得我们的 StartPresenter 真的不知道从哪里获取用户名吗?这个问题的原因是这个示例没有包含一个真实的 interactor 层。我们将在下一章中将 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 provider减少这个新创建的状态,并返回结果。
剧情预警:我们很想为您动态完成这项工作,但由于组合状态的通用结构,没有办法通过动态创建这种结构,所以我们陷入了困境。我们的最佳猜测是使用Sourcery生成AppReducer,您在这里可以找到相关的教程。
现在创建一个VISPERAppFactory
创建您的应用并返回它。完整的createApp()
函数现在应该看起来像这样。
func createApp() -> AnyVISPERApp<AppState> {
let initalState: AppState = AppState(userState: UserState(userName: "unknown stranger"))
let appReducer: AppReducer<AppState> = { (reducerProvider: ReducerProvider, action: Action, state: AppState) -> AppState in
let newState = AppState(userState: reducerProvider.reduce(action: action, state: state.userState))
return reducerProvider.reduce(action: action, state: newState)
}
let appFactory = VISPERAppFactory<AppState>()
return appFactory.makeApplication(initialState: initalState, appReducer: appReducer)
}
接下来,更改visperApp属性的类型为AnyVISPERApp,并用您新创建的createApp()
函数对其进行初始化。
您的AppDelegate现在应该像这样
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var visperApp: AnyVISPERApp<AppState>!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
self.visperApp = self.createApp()
//imagine you initialized your features here
self.window?.makeKeyAndVisible()
return true
}
func createApp() -> AnyVISPERApp<AppState> {
let initalState: AppState = AppState(userState: UserState(userName: "unknown stranger"))
let appReducer: AppReducer<AppState> = { (reducerProvider: ReducerProvider, action: Action, state: AppState) -> AppState in
let newState = AppState(userState: reducerProvider.reduce(action: action, state: state.userState))
return reducerProvider.reduce(action: action, state: newState)
}
let appFactory = VISPERAppFactory<AppState>()
return appFactory.makeApplication(initialState: initalState, appReducer: appReducer)
}
}
哇,要运行它需要这么多东西...
现在开始好了
现在是时候让您和我们的StartPresenter
高兴,通过给他访问状态。
我们通过将其userName
属性类型从String
更改为ObservableProperty
来开始这个过程。
ObservableProperty是一个值包装器,可以在其值更改时通知您。
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中,我们使用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。现在如果我们你在文本框中输入用户名,我们想要改变应用状态。由于我们的视图只需要关注查看东西和响应用户输入,状态改变应该在主持人(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来初始化主持人,让我们也将它赋予我们的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 ...
//
}
现在我们需要一个Reducer来处理当接收到ChangeUserNameAction
时的UserState
(和结果状态AppState
)的变化。
struct ChangeUserNameAction: Action {
let username: String?
}
它是一个非常简单的结构体,将我们的用户名字符串传递给更改应用状态的reducer。
在主持人中分发它很简单。
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的UserState
(和结果状态AppState
)变化的ChangeUserNameAction
。
但还有希望,构建这样一个东西很简单(甚至可以通过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组件。
应用
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
您基本上可以向一个FeatureObserver和功能添加到App中。
当添加功能时,FeatureObserver会被调用,并负责配置您的VISPER组件以提供您功能所实现的特性。
许多VISPER组件实现了它们自己的App、功能和FeatureObserver的子类型。
VISPER-Wireframe为您提供了
- WireframeApp
- ViewFeature
- PresenterFeature
- WireframeFeatureObserver
- ViewFeatureObserver
- PresenterFeatureObserver.
VISPER-Redux为您提供了
VISPER-Swift提供了一个VISPERApp,它结合了WireframeApp和ReduxApp的所有特性,并且用于大多数使用VISPER-Framework构建的App。
Wireframe
Wireframe管理VISPER应用程序中UIViewController的生命周期。它用于创建控制器并在控制器之间进行路由。它将ViewController的表示和创建逻辑从UIViewController本身分离出来。
VISPER-Wireframe组件包含Wireframe协议的实现。
您可以使用DefaultWireframeAppFactory来创建一个默认配置的WireframeApp。
let navigationController = UINavigationController()
let factory = DefaultWireframeAppFactory()
let wireframeApp = factory.makeApp()
wireframeApp.navigateOn(navigationController)
如果您不想创建WireframeApp,则使用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")!)
路由范式
需要记录,请查看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
每个存储都有一个特殊的定义:
public typealias AppReducer<State> = (_ ReducerProvider: ReducerProvider,_ action: Action, _ state: State) -> State
它用作存储的单个入口点。每当分发一个操作时,都会调用它来解决新的状态。由于我们的状态是通用的,因此需要将每个状态属性的创建委托给reducerProvider参数。
对于之前定义的AppState,AppReducer 应该是这样的
let appReducer = { (reducerProvider: ReducerProvider, action: Action, currentState: AppState) -> AppState in
let state = AppState(
userState: reducerProvider.reduce(action,currentState.userState),
todoListState: reducerProvider.reduce(action,currentState.todoListState),
imprintState : reducerProvider.reduce(action,currentState.imprintState)
)
return reducerProvider.reduce(action,state)
}
ReduxApp
创建一个Redux应用非常简单
let appState: AppState = AppState( ... create your state here ...)
let factory = ReduxAppFactory()
let app: ReduxApp<AppState> = factory.makeApplication(initialState: appState, appReducer: appReducer)
更改状态
在使用 VISPER-Redux 的应用中,当前状态被存储在一个中心 Store 实例中,该实例存在于一个类型为 Redux 的便利包装对象中。状态更改只能通过向 ActionDispatcher 分发一个操作(一个简单的消息对象)并在 Reducer(一个 reduce 函数或类型为 FunctionalReducer、ActionReducerType 或 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指定了在向 store 发送操作后应用程序状态如何更改。记住,操作仅描述发生了什么,但没有描述应用程序状态如何变化。VISPER Swift 中的reducer可以是reduce函数,也可以是类型为 FunctionalReducer、ActionReducerType 或 AsyncActionReducerType 的实例。
或 AsyncActionReducerType。
ReduceFuntion
减少函数实际上是一个简单的函数,它接受一个提供者、一个操作和一个状态,并返回相同类型的新状态。
let reduceFunction = { (provider: ReducerProvider, action: SetUsernameAction, state: UserState) -> UserState in
return UserState(userName: action.newUsername,
isAuthenticated: state.isAuthenticated)
}
reducerContainer.addReduceFunction(reduceFunction:reduceFunction)
功能性减少器
功能性减少器非常相似,只是减少器接受一个减少函数,并使用它来对状态进行减少。
let functionalReducer = FunctionalReducer(reduceFunction: reduceFunction)
reducerContainer.addReducer(reducer:functionalReducer)
操作减少器类型
操作类型减少器是操作减少器类型的类,它包含特定操作和状态类型的减少函数。
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)
异步操作减少器类型
异步减少器是异步操作减少器类型的减少器,它不返回新状态,而是调用一个带有新状态的完成函数。
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)
逻辑特性
您可以使用逻辑特性向您的应用程序添加一些减少器。
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 包含一个用于表示 AppState 变化的 ObservableProperty。这个 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 组件 pod 中实现。
- VISPER-Wireframe - 包含实现 VIPER-Application 中 wireframe 层的组件,它管理 ViewControllers 的显示和生命周期。
- VISPER-Presenter(swift / objc) - 包含实现 VIPER-Application 中表示层逻辑的组件。它包含一些 presenter 类来确定您的应用程序逻辑和视图逻辑。
- VISPER-Redux - 包含用于在 VIPER-Application 中表示交互层的Redux架构实现的组件。
- VISPER-Reactive - 简单的响应式属性实现,允许在 VISPER-Application 中使用响应式 Redux 架构。它可以通过 subspec VISPER-Rective/RxSwift 进行更新,以使用 RxSwift 框架。
- VISPER-Sourcery - 支持您通过为您创建一些必要的模板代码来创建 VISPER 应用程序的组件。
- VISPER-UIViewController (Swift) / (Objective-C) - 扩展 UIViewControllers 以通知其生命周期 (viewDidLoad 等) 的组件
- VISPER-Entity - 如果在 VISPER-Application 中不使用自定义层,则用于建模实体层的组件。