VISPER
VISPER 是一个组件化的库,它帮助您根据 VIPER 模式开发模块化应用程序。VISPER 包含多个组件,以创建灵活的架构同时不花费太多时间在 VIPER 上。
下面是使用 VISPER 构建的典型应用程序架构图。
好吧,我们知道这看起来有点复杂。
我们为此主题创建了一篇单独的文章,您可以在这里找到它:这里。
了解 VISPER 如何帮助您构建应用程序的最佳方式之一是查看其主组件。
最通用的组件是 App 协议,它帮助您将应用程序组合为名为 功能 的独立模块。每个功能都创建并配置应用程序的独立部分。
由于 ViewControllers 的展示、创建和管理是您应用程序的一个重要部分,因此应该有一个单独的组件来负责 ViewControllers 的展示和生命周期。这项任务由 Wireframe 完成。
Wireframe 允许您通过简单的 URL(称为 Route)路由到特定的 ViewController。它将 ViewController 的创建(由 ViewFeature 完成)与其展示(由一个名叫 RoutingPresenter 的神秘人物完成)分开。
由于我们在大量 ViewControllers 上进行了太多的战斗,我们希望我们的视图非常愚蠢。这为 Presenter 奠定了基础。Wireframe 在展示 ViewController 之前,从 PresenterFeature 获取所有负责的 Presenters。它给了他们机会在实际上展示之前用一些数据绑定和行为丰富这个愚蠢的东西。
下一个重大挑战是管理应用程序的状态。VISPER 决定在交互层中完成这项任务,并支持您使用 Redux 架构 来管理应用程序状态及其状态转换。
您可以通过向 ActionDispatcher 提交一个 Action 来在 presenter 中触发状态更改,并从另一方面观察状态变化以更新您的视图。更多关于这个主题的信息可以在 这里 找到。
索引
入门
由于VISPER由几个可以单独使用的独立组件组成,让我们先从WireframeApp开始,了解视图控制器的路由和生命周期。
在下一步中,我们将通过 Redux 相关内容扩展此示例。
开始使用线框图
从创建一个使用UINavigatorController的简单项目开始。
spoiler:VISPER可以管理您选择的任何容器视图控制器,但由于默认实现中已预先配置了UINavigatorController,因此我们很容易从这里开始。
现在在您的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创建了一个WireframeApp,它已经配置好,可以与UINavigatorController一起使用。如果您想了解更多关于线框图的信息,可以在这里找到这些信息,但我们暂时先完成WireframeApp的创建。
在下一步中,您必须告诉您的WireframeApp将要用于导航和路由的UINavigatorController。我们只需要一行代码就可以完成这个任务...
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)
}
正如你可能猜到的,我们的UINavigatorController所处的位置无关紧要,你甚至也可以将它在UITabbarController或UISplitViewController中。VISPER将使用给它的最后一个UINavigatorController通过visperApp.navigateOn(controller)方法。
它不会为您保留它!所以如果它变得未保留,它将消失,VISPER会抱怨它不知道使用哪个UINavigatorController!
您的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,为它提供“import VISPER”语句。这是一个拙劣的技巧,但它允许你使用“import VISPER”语句为每个VISPER类或协议,即使它们存在于我们的子Pod中。
import VISPER
class StartFeature: ViewFeature {
let routePattern: String
// you can ignore the priority for the moment
// it is sometimes needed when you want to "override"
// an already added Feature
let priority: Int = 0
init(routePattern: String){
self.routePattern = routePattern
}
// create a blue controller which will be created when the "blue" route is called
func makeController(routeResult: RouteResult) throws -> UIViewController {
let controller = StartViewController()
controller.buttonTitle = "Hello unknown User!"
return controller
}
// return a routing option to show how this controller should be presented
func makeOption(routeResult: RouteResult) -> RoutingOption {
return DefaultRoutingOptionPush()
}
}
如你所见,必须实现两个方法和一个属性以向Wireframe提供ViewController。
让我们从routePattern开始。它是一个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-Parameter包含一些在创建viewController时可能有用的上下文信息,尽管我们在这里不需要它(稍后查看RouteResult部分)
makeOption(routeResult:)
要复杂一些(当需要时查看RoutingOption部分)。它定义了Creamframe默认情况下如何显示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层的一拳男孩的人。我们有这个表示者。
表示者协议相当简单
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,向buttonTitle添加一些信息,并添加一些行为到按钮的点击事件。
让我们假设我们的演示者从其环境获取用户名信息作为依赖,我们稍后会在这个交互器/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
}
点击视图按钮现在会输出调试信息"很高兴认识你,未知的人!”,但为何不显示一个包含此信息的警报消息呢?
接受挑战吗?
让我们从创建一个新的功能开始,创建一个显示...
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应该包含我们应用的全局状态。由于整个应用的状态可能相当复杂,因此将状态组合成不同的子状态是一种更好的做法,以便更本地化地处理状态复杂性。
尽管我们的状态并不真正复杂,但这样做是一个好主意,以展示如果需要会如何进行状态组合。
下一步应该是用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更改为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存在的时间内(这应该就是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
更改了URL路径,甚至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"}))
现在构建此应用程序会产生一个使用app状态作为其响应式数据源的正在运行的应用程序。
好吧,如果您不改变状态,那么有一个响应式数据源是相当无聊的。是时候引入一些状态变化了。
我们从向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)
}
}
这里并没有像魔法一样发生什么,它只是我们的普通视图控制器 UIKitViewController,其中包含一个按钮和一个UITextField,位于其标题视图中。现在,如果我们在文本字段中输入用户名,我们就想改变我们的应用程序状态。由于我们的视图应该只关心查看内容和对用户输入作出响应,因此触发状态变化应该在展示者中进行。
展示者通过向Action(一个简单的消息对象)和ActionDispatcher(更多内容请参阅这里)中的ActionDispatcher发送来触发状态变化。所以让我们给他提供一个这样的东西。
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 ...
//
}
现在我们需要一个Action由StartPresenter
分发。
struct ChangeUserNameAction: Action {
let username: String?
}
它只是一个非常简单的结构体,携带我们的用户名字符串到更改应用程序状态的计算器。
在展示者中分发它很容易。
func addPresentationLogic(routeResult: RouteResult, controller: UIViewController) throws {
guard let controller = controller as? StartViewController else {
fatalError("needs a StartViewController")
}
//ignore the whole view observing stuff normally done here
controller.nameChanged = { [weak self](_, text) in
self?.actionDipatcher.dispatch(ChangeUserNameAction(username: text))
}
}
现在您可以构建应用程序了,但如果您输入用户名,应用程序状态不会发生变化。
那是什么鬼?
嗯,没有还原器处理在接收到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协议的一个实例,它允许您通过一个代表您应用程序不同功能的功能来配置应用程序,该功能配置了它所需使用的所有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)
}
功能与功能观察者
每当添加一个 FeatureObserver,就会调用它。它负责配置您的 VISPER 组件以提供由您的 Feature 实现的功能。
许多 VISPER 组件实现了自己子类的 App、Feature 和 FeatureObserver。
VISPER-Wireframe 为您提供了以下内容
- WireframeApp
- ViewFeature
- PresenterFeature
- WireframeFeatureObserver
- ViewFeatureObserver
- PresenterFeatureObserver.
VISPER-Redux 提供以下内容
VISPER-Swift 提供了一个 VISPERApp,它结合了 WireframeApp 和 ReduxApp 的所有特性,并用于大多数使用 VISPER-Framework 构建的 App。
Wireframe
Wireframe 管理了 VISPER-应用程序中 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,它向 wireframe 提供一个 ViewController 和一些 RoutingOptions,用以定义控制器如何展示。
class ExampleViewFeature: ViewFeature {
var routePattern: String = "/exampleView"
var priority: Int = 0
//controller will be pushed on current active navigation controller
func makeOption(routeResult: RouteResult) -> RoutingOption {
return DefaultRoutingOptionPush()
}
func makeController(routeResult: RouteResult) throws -> UIViewController {
let controller = UIViewController()
return controller
}
}
将其添加到您的 WireframeApp
let feature = ExampleFeature()
wireframeApp.add(feature: feature)
或您的 Wireframe
let feature = ExampleFeature()
wireframe.add(controllerProvider: feature, priority: feature.priority)
wireframe.add(optionProvider: feature, priority: feature.priority)
try wireframe.add(routePattern: feature.routePattern)
您现在可以路由到 ExampleFeature 提供的控制器
try wireframe.route(url: URL(string: "/exampleView")!)
以下是一个使用 wireframe 的 VISPER 完整示例
Routepatterns
必须进行文档说明,请参阅 JLRoutes 中 RoutePatterns 的定义(我们曾借此中获得灵感)。
VISPER-Redux
VISPER-Redux 是 Swift 中 Redux 架构的一种实现。
它为您提供了一种应用架构,用于解决分布式应用状态和状态变更问题。它是许多基于 VISPER 框架的 Apps 中交互层实现的体现。
如果您想了解更多关于 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 函数或 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
Reducer 指定应用程序的状态如何响应用户发送到 Store 的动作。请记住,动作仅描述发生了什么,但没有描述应用程序的状态如何改变。在 VISPER Swift 中的 Reducer 可以是一个 reduce 函数,也可以是 FunctionalReducer、ActionReducerType 或 AsyncActionReducerType 的实例。
。
ReduceFuntion
reduce 函数只是一个简单的函数,获取一个提供者、一个动作和一个状态,并返回同一类型的新状态。
let reduceFunction = { (provider: ReducerProvider, action: SetUsernameAction, state: UserState) -> UserState in
return UserState(userName: action.newUsername,
isAuthenticated: state.isAuthenticated)
}
reducerContainer.addReduceFunction(reduceFunction:reduceFunction)
FunctionalReducer
函数式reducer非常相似,它接收一个reduce函数并使用它来减少状态。
let functionalReducer = FunctionalReducer(reduceFunction: reduceFunction)
reducerContainer.addReducer(reducer:functionalReducer)
ActionReducerType
ActionReducerType是一类reducer,包含特定动作和状态类型的reduce函数。
struct SetUsernameReducer: ActionReducerType {
typealias ReducerStateType = UserState
typealias ReducerActionType = SetUsernameAction
func reduce(provider: ReducerProvider,
action: SetUsernameAction,
state: UserState) -> UserState {
return UserState(userName: action.newUsername,
isAuthenticated: state.isAuthenticated)
}
}
let reducer = SetUsernameReducer()
reducerContainer.addReducer(reducer:reducer)
AsyncActionReducerType
异步reducer是AsyncActionReducerType的一个reducer,它不返回新的状态,而是调用一个带有新状态的完成调用。
struct SetUsernameReducer: AsyncActionReducer {
typealias ReducerStateType = UserState
typealias ReducerActionType = SetUsernameAction
let currentState: ObserveableProperty<UserState>
func reduce(provider: ReducerProvider,
action: SetUsernameAction,
completion: @escaping (_ newState: UserState) -> Void) {
let newState = UserState(userName: action.newUsername,
isAuthenticated: self.currentState.value.isAuthenticated)
completion(newState)
}
}
let reducer = SetUsernameReducer(currentState: app.state.map{ $0.userState })
reducerContainer.addReducer(reducer:reducer)
LogicFeature
可以使用LogicFeature向您的应用程序添加一些reducer。
import VISPER_Redux
class ExampleLogicFeature: LogicFeature {
func injectReducers(container: ReducerContainer) {
let reducer = SetUsernameReducer()
container.addReducer(reducer: incrementReducer)
}
}
let logicFeature = ExampleLogicFeature()
app.add(feature: let logicFeature)
观察状态变化
观察状态变化很简单。只需观察您应用程序的状态。
//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 [链接](https://rawgit.com/barteljan/VISPER/master/docs/VISPER-Redux/index.html) 包含一个ObservableProperty来表示变化的AppState。ObservableProperty 允许您订阅状态变化,并将其映射到一个RxSwift-Observable。它在VISPER-Reactive组件中实现。
SubscriptionReferenceBag
需要文档说明
所有可用的VISPER组件完整列表
- VISPER - 一个便利的导入包装器,用于使用单个导入包含所有VISPER组件。它包含一些旧的组件,以支持与之前VISPER版本的后向兼容。
- VISPER-Swift - VISPER-Framework的所有Swift组件的集合,以及它们所有依赖项的便利导入包装器。
- VISPER-Objc - 一个包装核心VISPER类,以便在objc代码库中使用的包装器。
- VISPER-Core - 适用于在您的功能的不同组件之间进行通信的一些常用核心协议。如果您希望将VISPER组件包含到自己的项目和组件中,应使用此pod。其他VISPER组件pod实现了其协议。
- VISPER-Wireframe - 包含VIPER应用程序中wireframe层实现的组件,它管理您ViewController的展示和生命周期。
- VISPER-Presenter(swift / objc) - 包含VIPER应用程序中展示层实现的组件。它包含一些presenter类,用于区分您的应用逻辑和视图逻辑。
- VISPER-Redux - 包含Redux架构实现的组件,许多VISPER应用程序中使用它来表示VIPER应用程序中的interactor层。
- VISPER-Reactive - 简单的响应式属性实现,允许在VISPER应用程序中使用响应式Redux架构。它可以通过VISPER-Rective/RxSwift subspec更新,以使用RxSwift框架。
- VISPER-Sourcery - 支持您通过为您创建一些必要的样板代码来创建VISPER应用程序的组件。
- VISPER-UIViewController (swift / objc) - 扩展UIViewControllers以通知presenter其生命周期(viewDidLoad等)的组件。
- VISPER-Entity - 适用于在您的VISPER应用程序中不使用自定义层时建模entity层的组件。