VISPER-Presenter 5.0.1

VISPER-Presenter 5.0.1

Jan Bartel 维护。



VISPER

Version License Platform

VISPER 是一个基于组件的库,帮助您使用 VIPER 模式开发模块化应用。VISPER 包含多个组件,以创建无需花费太多时间在 VIPER 上即可灵活的架构。

以下图像展示了使用 VISPER 构建的典型应用架构。

好吧...我们知道这有一点复杂。

我们为此创建了一篇单独的文章,深入研究这个话题。您可以在这里找到。

Architecture

要了解 VISPER 如何帮助您构建应用,最简单的方法是看看它的主要组件。

最通用的组件是 App 协议,它帮助您将应用程序组合成分离的模块,称为 功能。每个功能都创建和配置应用程序的一个独立部分。

由于展示,创建和管理 ViewControllers 是您应用程序的一个重要部分,应该有一个负责展示和 ViewControllers 生命周期的独立组件。这项工作由 Wireframe 负责。

Wireframe 允许您通过一个简单的 URL(称为 Route)路由到特定的 ViewController。它将 ViewController 的创建(由 ViewFeature 完成)与其展示(由名为 RoutingPresenter 的奇怪角色完成)分离。

由于我们已经与庞大的 ViewController 作战太多次,我们希望视图非常愚笨。这为 Presenter 预留了舞台。wireframe 在实际展示 ViewController 之前,从 PresenterFeature 中请求所有的负责 Presenters。它们有机会在实际上展示之前,通过一些数据绑定和行为使这愚蠢的事物变得丰富。

下一个伟大的挑战是管理您应用程序的状态。VISPER 决定在交互层中这样做,并支持您使用 Redux-Architecture 管理应用程序状态及其状态转换。

您可以通过向 ActionDispatcher 提交一个 Action 来在 presenter 中触发状态更改,并在另一边观察状态更改以更新视图。有关此主题的更多内容,请参阅此处

索引

入门指南

由于VISPER由几个独立的组件组成,这些组件可以单独使用,所以让我们从WireframeApp开始,了解路由和ViewController的生命周期。

在接下来的步骤中,我们将扩展此示例,添加一些Redis内容。

开始使用线框图

从创建一个使用UINavigationController的简单项目开始。

剧透:VISPER可以管理您选择的任何容器ViewController,但由于在默认实现中UINavigationController已预先配置,因此使用它很容易入门。

现在在您的AppDelegate中创建一个WireframeApp

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var visperApp: WireframeApp!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        let window = UIWindow(frame: UIScreen.main.bounds)
        self.window = window
        
        let factory = DefaultWireframeAppFactory()
        let visperApp = factory.makeApp()
        self.visperApp = visperApp
        
        // the rest of your implementation will follow soon ;)    
    }

DefaultWireframeAppFactory创建一个已经预配置用于UINavigationController的WireframeApp。如果您想了解更多关于线框图的信息,您可以在这里找到相关信息,但现在让我们先完成WireframeApp的创建。

在下一步中,您必须告诉您的WireframeApp应该使用哪个UINavigationController进行导航和路由。为此只需一行代码...

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
    //just imagine we created your visperApp here
    
    let navigationController = UINavigationController()
    window.rootViewController = navigationController
    
    //tell visper to work with this UINavigationController
    visperApp.navigateOn(navigationController)
        
}

正如您所猜疑的,我们的UINavigationController所在位置完全无关紧要,您也可以将其放入UITabbarController或UISplitViewController中。VISPER会使用visperApp.navigateOn(controller)方法给出的最后一个UINavigationController。

它不会为您保留它!所以如果它不再被保留,它就会消失,而VISPER会因为不知道使用哪个UINavigationController而抱怨!

您的WireframeApp现在已经初始化完毕,准备好进行路由,但它还没有ViewController来创建,因此让我们创建一个ViewFeature来执行一些操作...

控制器只需要是一个POVC(普通的旧视图控制器😅)即可,它不必了解任何关于VISPER的信息。所以,我们来创建一个

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"的协议,必须实现以提供给线框ViewController。在VISPER-Pod中我们定义了一些typealiases,以便提供"import VISPER"语句。这是个不道德的伎俩,但它允许你为每个VISPER类或协议都使用"import VISPER"语句,即使它们存在于我们的子Pod中。

import VISPER

class StartFeature: ViewFeature {
    
    let routePattern: String
    
    // you can ignore the priority for the moment
    // it is sometimes needed when you want to "override"
    // an already added Feature
    let priority: Int = 0
    
    init(routePattern: String){
        self.routePattern = routePattern
    }
    
    // create a blue controller which will be created when the "blue" route is called
    func makeController(routeResult: RouteResult) throws -> UIViewController {
        let controller = StartViewController()
        controller.buttonTitle = "Hello unknown User!"
        return controller
    }
    
    // return a routing option to show how this controller should be presented
    func makeOption(routeResult: RouteResult) -> RoutingOption {
        return DefaultRoutingOptionPush()
    }
}

如你所见,有两组方法和一个属性必须实现,以提供给线框一个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部分)。它定义了线框默认如何呈现ViewController。返回DefaultRoutingOptionPush将导致线框只是将我们的ViewController推送到其当前的 UINavigationController上。

好吧,这很简单😇,现在让我们在我们的应用中注册我们的新功能...返回你的AppDelegate并添加它

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,它不能应对除了视图之外的其他任何事情……

我们需要的是一个人,将愚蠢的视图与交互层和线框层的one-trick ponies连接起来。我们可以通过展示者来实现。

展示者协议非常简单

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
}

现在点击视图按钮将导致调试输出“很高兴见到你,未知的小伙子!”,但展示一个带有这条消息的警告信息不是更好吗?

接受挑战了吗?

让我们先从创建一个新的Feature开始,创建一个展示AlertController的...

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来展示它。

Voila!一个向我们问候的警告视图出现了。

对于这样一个AlertViewController来说,发生了很多东西,但它具有良好的扩展性,可以帮助你保持更大的ViewController简单、分离,并保持清晰的架构,将展示和领域逻辑从ViewController中分离出来,放在它们应有的位置。

你还记得我们的StartPresenter实际上并不知道从哪里检索用户名吗?这个问题出现的原因是,这个示例没有包含真正的交互层。我们将通过向这个示例添加Redux架构来解决这个问题的下一个章节。

你可以在VISPER.xcworkspace的Wireframe-Example中找到这个教程的代码。

添加Redux

悬念:如果你没有完成上一章,可以使用VISPER.xcworkspace中的Wireframe-Example作为此教程的起点。你需要将工作区的Podfile中的"Wireframe-Example"部分替换为以下代码

target 'VISPER-Wireframe-Example' do
   pod 'VISPER-Wireframe', :path => '../'
   pod 'VISPER-Core', :path => '../'
   pod 'VISPER-UIViewController', :path => '../'
   pod 'VISPER-Objc', :path => '../'
   pod 'VISPER', :path => '../'
end   

之后运行pod install

如果你对Redux这个概念还不熟悉,请从阅读以下两篇文章开始。

如果你已经了解 Redux 但还没有理解它的所有细节,思考一下也阅读它们😛.

完成这部分后,快速查看上一章的项目,并创建两个状态。

struct UserState: Equatable {
    let userName: String
}

struct AppState: Equatable {
    let userState: UserState?
}

剧透:让你的 AppState 符合 Equatable 是一个好的做法,因为它使得处理状态变化更简单。

AppState 应该包含我们应用程序的全部状态。由于整个应用程序的状态可以非常复杂,因此最佳实践是将它组合成不同的子状态,从而以更本地的形式克服状态复杂性。

尽管我们的状态并不真的复杂,但开始这样做是个好主意,以展示如果需要,状态组合应该如何进行。

下一步应该用 AnyVISPERApp< AppState > 替换我们的 WireframeApp。这为我们提供了一个 Redux 对象来管理我们的状态变化。

在你的 AppDelegate 中创建一个新的方法。

func createApp() -> AnyVISPERApp<AppState> {
    fatalError("not implemented")
}

继续创建一个当应用程序加载时使用的初始状态。

let initalState: AppState = AppState(userState: UserState(userName: "unknown stranger"))

现在该写一些模板代码,并编写 AppReducer 函数,它负责组合和减少我们的 AppState。

let appReducer: AppReducer<AppState> = { (reducerProvider: ReducerProvider, action: Action, state: AppState) -> AppState in
    let newState = AppState(userState: reducerProvider.reduce(action: action, state: state.userState))
    return reducerProvider.reduce(action: action, state: newState)
}

它的结构始终相同,它创建一个新的 AppState 并使用它的 reducerProvider 参数将所有子状态与之结合。然后,它使用 reducer provider 减少新创建的状态,并返回结果。

剧透:我们很想帮你以这种方式编程,但由于组合的 AppState 是一个通用的 struct,并且没有创建这种 struct 的动态方法,所以我们陷入了困境。我们最好的猜测是使用 Sourcery 生成 AppReducer,你可以在 这里找到如何做的一个教程。

现在创建一个 VISPERAppFactory< AppState >,创建你的应用程序并返回它。现在完整的 createApp() 函数看起来应该是这样的。

func createApp() -> AnyVISPERApp<AppState> {
    let initalState: AppState = AppState(userState: UserState(userName: "unknown stranger"))
    
    let appReducer: AppReducer<AppState> = { (reducerProvider: ReducerProvider, action: Action, state: AppState) -> AppState in
        let newState = AppState(userState: reducerProvider.reduce(action: action, state: state.userState))
        return reducerProvider.reduce(action: action, state: newState)
    }
    
    let appFactory = VISPERAppFactory<AppState>()
    return appFactory.makeApplication(initialState: initalState, appReducer: appReducer)
}

接下来,将你的 visperApp 属性的类型更改为 AnyVISPERApp,并用你刚刚创建的 createApp() 函数初始化它。

你的 AppDelegate 现在应该看起来像这样:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var window: UIWindow?
    var visperApp: AnyVISPERApp<AppState>!
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        let window = UIWindow(frame: UIScreen.main.bounds)
        self.window = window
        
        self.visperApp = self.createApp()
        
        //imagine you initialized your features here
        
        self.window?.makeKeyAndVisible()
        return true
    }
    
    func createApp() -> AnyVISPERApp<AppState> {
        let initalState: AppState = AppState(userState: UserState(userName: "unknown stranger"))
        
        let appReducer: AppReducer<AppState> = { (reducerProvider: ReducerProvider, action: Action, state: AppState) -> AppState in
            let newState = AppState(userState: reducerProvider.reduce(action: action, state: state.userState))
            return reducerProvider.reduce(action: action, state: newState)
        }
        
        let appFactory = VISPERAppFactory<AppState>()
        return appFactory.makeApplication(initialState: initalState, appReducer: appReducer)
    }
    
}

哇,这太多了,要让它运行起来...

现在情况在好转😊.

该是我们让我们的 StartPresenter 高兴的时候了,我们给它访问状态。

我们从将其 userName 属性类型从 String 更改为 ObservableProperty 开始。

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` 参数。

让我们通过将这样的属性注入到 StartFeatureStartPresenter 中来解决这个问题。

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在它的标题视图中。现在我们想改变我们的appstate,如果你在文本框中输入你的用户名。由于我们的视图应该只关注查看内容并对用户输入做出响应,因此触发状态更改应该在presenter中完成。

presenter通过向ActionDispatcher(了解更多这里)发送一个Action(一个简单的消息对象)来触发状态更改。所以让我们提供给他这样一个东西。

class StartPresenter: Presenter {
    
    //
    // imagine some old properties here ...
    // 
    let actionDipatcher: ActionDispatcher
    

    init(userName: ObservableProperty<String?>, 
        wireframe: Wireframe, 
  actionDipatcher: ActionDispatcher) {
        
        //use map to translate from ObservableProperty<String?> to ObservableProperty<String>
        self.userName = userName.map({ (name) -> String in
            guard let name = name, name.count > 0 else { return "unknown person"}
            return name
        })
        
        self.wireframe = wireframe
        
        //this one is new
        self.actionDipatcher = actionDipatcher
    }
    
    //you already know the rest of our StartPresenter
}

由于我们现在需要一个ActionDispatcher来初始化presenter,让我们将它也提供给我们的StartFeature

class StartFeature: ViewFeature {
    
    //
    // imagine some old properties here ...
    // 
    let actionDispatcher: ActionDispatcher
    
    init(routePattern: String, 
            wireframe: Wireframe,
     actionDispatcher: ActionDispatcher, 
             userName: ObservableProperty<String?>){
        self.routePattern = routePattern
        self.wireframe = wireframe
        self.userName = userName
        self.actionDispatcher = actionDispatcher
    }
    
    //
    // and the rest of it's implementation following here ...
    // 
}

extension StartFeature: PresenterFeature {
    
    func makePresenters(routeResult: RouteResult, controller: UIViewController) throws -> [Presenter] {
        return [StartPresenter(userName: self.userName, 
                              wireframe: self.wireframe, 
                        actionDipatcher: self.actionDispatcher)]
    }
    
}

并将其注入我们的AppDelegate中。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
    //
    // some intialization before ...
    //
       
    let startFeature = StartFeature(routePattern: "/start",
                                       wireframe: visperApp.wireframe,
                                actionDispatcher: visperApp.redux.actionDispatcher,
                                        userName: visperApp.redux.store.observableState.map({ return $0.userState.userName }))
    try! visperApp.add(feature: startFeature)
    
    //
    // some routing afterwards ...
    //
}

现在我们需要一个StartPresenter将要分发的Action。

struct ChangeUserNameAction: Action {
    let username: String?
}

它只是一个非常简单的结构,承载(有些人会说“传递”)我们的用户名字符串到改变appstate的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))
    }
}

现在你可以构建你的应用了,但你输入用户名时appstate并没有改变。

那是什么鬼?

事实上,没有Reducer处理接收ChangeUserNameAction时改变UserState(以及作为结果的 AppState)的情况。

但是还有希望,构建这样的东西很容易(它可以由Sourcery生成)。

struct ChangeUserNameReducer: ActionReducerType {

    typealias ReducerStateType = UserState
    typealias ReducerActionType = ChangeUserNameAction
    
    func reduce(provider: ReducerProvider, action: ChangeUserNameAction, state: UserState) -> UserState {
        return UserState(userName: action.username)
    }
}

将其添加到StartFeature中后,就完成了。

extension StartFeature: LogicFeature {
    func injectReducers(container: ReducerContainer) {
        container.addReducer(reducer: ChangeUserNameReducer())
    }
}

现在启动你的应用应该会在你输入用户名时改变按钮的标题。

您可以在VISPER.xcworkspace的VISPER-Swift-Example中找到此教程的代码。

组件

应用

你的VISPER应用的核心组件是App协议的一个实例,它允许你通过一个代表你的应用的独立功能的Feature来配置你的应用,并配置所有使用的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

您可以基本添加一些功能观察器(FeatureObserver)功能(Feature)到应用程序(App)中。

每当添加一个功能时,都会调用功能观察器,该观察器负责配置您的VISPER组件以提供您实现的功能。

许多VISPER组件实现了自己的应用程序、功能和功能观察器的子类型。

VISPER-Wireframe为您提供以下内容:

VISPER-Redux为您提供:

VISPER-Swift提供一个结合了WireframeAppReduxApp所有特点的VISPERApp,该App在VISPER框架创建的大多数应用程序中使用。

Wireframe

Wireframe管理VISPER应用程序中UIViewController的生命周期。它用于创建控制器并从一个控制器路由到另一个控制器,它将ViewController的展示和创建逻辑与UIViewController本身分离。

VISPER-Wireframe组件包含对Wireframe协议的实现。

您可以使用DefaultWireframeAppFactory来创建一个具有默认配置的WireframeApp

let navigationController = UINavigationController()
let factory = DefaultWireframeAppFactory()
let wireframeApp = factory.makeApp()
wireframeApp.navigateOn(navigationController)

如果您想要创建一个不创建WireframeAppWireframe,请使用WireframeFactory

let factory = WireframeFactory()
let wireframe = factory.makeWireframe()

现在创建一个提供ViewController和一些路由选项(定义如何展示控制器)的ViewFeature,并将其添加到您的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是Redux架构在Swift中的实现。

它为您提供了一个应用架构,以应对分布式应用状态和状态变化的问题。它是许多基于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的实例、ActionReducerTypeAsyncActionReducerType)中创建新的状态

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函数,或类型为FunctionalReducerActionReducerTypeAsyncActionReducerType的实例

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与functional reducer非常相似,只是reducer接受一个reduce函数并使用它来减少状态。

let functionalReducer = FunctionalReducer(reduceFunction: reduceFunction)
reducerContainer.addReducer(reducer:functionalReducer)
ActionReducerType

ActionReducerType 类型包含针对特定操作和状态类型的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向您的应用程序添加一些削减排度。

import VISPER_Redux

class ExampleLogicFeature: LogicFeature {

    func injectReducers(container: ReducerContainer) {
        let reducer = SetUsernameReducer()
        container.addReducer(reducer: incrementReducer)
    }

}


let logicFeature = ExampleLogicFeature()
app.add(feature: let logicFeature)

Observing state change

观察状态变化很简单。只需观察您应用的状态。

//get your reference bag to retain subscription
let referenceBag: SubscriptionReferenceBag = self.referenceBag


//subscribe to the state
let subscription = app.state.subscribe { appState in
    print("New username is:\(appState.userState.userName)")                                   
}

//retain subscription in your reference bag
referenceBag.addReference(reference: subscription)

VISPER-Redux中包含了一个表示状态变化的ObservablePropertyObservableProperty允许您订阅状态变化,并且可以被映射到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层的组件,它管理您观控控制器(ViewControllers)的表示和生命周期。
  • VISPER-Presenter(swift / objc) - 包含在VIPER应用中实施呈现层(presentation layer)的组件。它包含一些显示类,以便将应用逻辑与视图逻辑分开。
  • VISPER-Redux - 包含实现Redux架构的组件,许多VISPER应用使用Redux架构来表示VIPER应用中的交互层。
  • VISPER-Reactive - 简单的响应式属性实现,允许在VISPER应用中使用响应式Redux架构。它可以通过subspec VISPER-Rective/RxSwift来更新,以使用RxSwift框架。
  • VISPER-Sourcery - 支持您通过为创建一些必要的模板代码来创建VISPER应用。
  • VISPER-UIViewController (swift) / (objc) - 一个扩展UIViewControllers以通知其生命周期(例如 viewDidLoad)等事件的组件。
  • VISPER-Entity - 如果您在 VISPER-Application 中不使用自定义层,则用于模拟实体层的组件。