VISPER-Redux 5.0.0

VISPER-Redux 5.0.0

Jan Bartel 维护。



 
依赖
VISPER-Core~> 5.0.0
VISPER-Reactive~> 5.0.0
 

VISPER

Version License Platform

VISPER 是一个基于组件的库,它帮助您基于 VIPER 模式开发模块化应用程序。VISPER 包含几个组件,以创建一个灵活的架构,同时不会花费太多时间在 VIPER 上。

以下图片显示了使用 VISPER 构建的一个典型应用程序的架构。

好吧,我们知道这看起来有点复杂。

我们创建了一篇独立文章来深入探讨这个话题。您可以在 这里 找到。

Architecture

了解 VISPER 如何帮助您构建应用程序的最好方式是查看它的主要组件。

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

由于视图控制器的前台、创建和管理是您应用程序的重要部分,因此应该有一个负责视图控制器的前台和生命周期的独立组件。这个任务由 线框 完成。

线框 允许您通过简单的 URL(称为路由)导航到特定的 ViewController。它将 ViewController 的创建(由 ViewFeature 完成)与其展示(由名为 RoutingPresenter 的怪人完成)分开。

由于我们与庞大的 ViewControllers 作战了太多,我们希望我们的视图相当愚蠢。这为 Presenter 做了准备。线框在展示 ViewController 之前从 PresenterFeature 中请求所有负责的 Presenters。它们让他们有机会在实际上展示之前用一些数据绑定和行为丰富这件愚蠢的东西。

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

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

索引

入门

由于VISPER由几个独立的组件组成,这些组件可以单独使用,让我们放松一下,先从一个WireframeApp开始,以便了解视图控制器的工作方式和路由。

在下一步中,我们将通过一些Redux内容扩展此示例。

从线框开始入门

从一个使用UIImagePickerController的项目开始。

注意:VISPER可以管理您选择的任何容器视图控制器,但由于UINavigationController在默认实现中已预配置,因此我们可以从这里开始。

现在在AppDelegate中创建一个WireframeApp。

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var visperApp: WireframeApp!

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

默认的WireframeAppFactory创建一个WireframeApp,该App已配置为与UINavigationController一起使用。如果您想了解更多关于线框的信息,可以在这里找到相关信息,但现在我们先完成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(Plain Old View Controller😅),它不需要了解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"-Pod中的一种协议,必须实现以便将ViewController提供给Wireframe。我们在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开始。这是一个描述用于匹配此控制器的路由的字符串。

这意味着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推送到当前的UIResponderNavigationController。

好吧,这很简单😇,现在让我们在我们的应用程序中注册我们的新功能……回到您的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,他无法处理除视图以外的任何东西……

我们需要的人是连接愚蠢的视图和我们的交互器和Wireframe层中的one-trick ponys的人。我们可以用一个演示者来实现这一点。

演示者协议相当简单

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 层中稍后添加该信息。

要将演示者添加到我们的应用中,我们只需要将新的协议 PresenterFeature 通过扩展我们的 StartFeature

extension StartFeature: PresenterFeature {
    func makePresenters(routeResult: RouteResult, controller: UIViewController) throws -> [Presenter] {
        return [StartPresenter(userName: "unknown guy")]
    }    
}

并从 StartFeature 中移除以下令人讨厌的行(它应该创建组件,添加一些数据是演示者的工作)。

func makeController(routeResult: RouteResult) throws -> UIViewController {
    let controller = StartViewController()
    // remove the following line, our feature shouldn't care about usernames ...
    // controller.buttonTitle = "Hello unknown User!"
    return controller
}

点击视图按钮现在将生成调试输出 "很高兴认识你,未知的人!"!,但这不是更好的用一条消息展示一个提示信息吗?

接受挑战了吗?

从创建一个新的特征开始,创建一个 UIAlertController 以显示...

class MessageFeature: ViewFeature {
    
    var routePattern: String = "/message/:username"
    var priority: Int = 0
    
    
    func makeController(routeResult: RouteResult) throws -> UIViewController {
        
        let username: String = routeResult.parameters["username"] as? String ?? "unknown"
        
        let alert = UIAlertController(title: nil,
                                    message: "Nice to meet you \(username.removingPercentEncoding!)",
                             preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "Goodbye", style: .cancel, handler: nil))
        
        return alert
    }
    
    func makeOption(routeResult: RouteResult) -> RoutingOption {
        return DefaultRoutingOptionModal(animated: true,
                                         presentationStyle: .popover)
    }
    
}

这里有很多东西在进行,让我们查看我们所做的一切...

首先看看路由模式 /message/:username,它不是一个常见的可匹配字符串。在 :username 中定义了一个路由参数,名为 'username'。此路由将匹配任何以 /message/ 开头的 URL,其第二个部分被解释为路由参数 username。如果你的路由 URL 为 URL(string: '/message/Max%20Mustermann'),路由参数 'username' 将是 'Max Mustermann'

第二个是 makeController(routeResult:) 函数,它从它的 RoutingResult 中提取路由参数 'username'

最后,makeOption(routeResult:) 函数创建一个 DefaultRoutingOptionModal,这导致线框以模态视图控制器的方式呈现为 .popover 样式。

实现所有这些后,是时候在 AppDelegate 中向我们的 WireframeApp 添加新的功能了。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        //imagine visperApp is already initialized 
              
        let messageFeature = MessageFeature()
        try! visperApp.add(feature: messageFeature)
        
        //followed by other stuff
}

并修改我们的 StartPresenter 以使用 Wireframe 转至消息功能。

class StartPresenter: Presenter {
    
    var userName: String
    let wireframe: Wireframe
    
    init(userName: String, wireframe: Wireframe) {
        self.userName = userName
        self.wireframe = wireframe
    }
    
    func addPresentationLogic(routeResult: RouteResult, controller: UIViewController) throws {
        
        guard let controller = controller as? StartViewController else {
            fatalError("needs a StartViewController")
        }
        
        controller.buttonTitle = "Hello \(self.userName)"
        
        controller.tapEvent = { [weak self] (_) in
            guard let presenter = self else { return }
            let path = "/message/\(presenter.userName)".addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
            let url = URL(string:path)
            try! presenter.wireframe.route(url: url!)
        }
        
    }
}

现有内容发生在 tapEvent 闭包中。演示者创建了一个 URL(string:"/message/unknown%20guy") 并告诉线框将路由到此 URL(如果我们假装用户名仍然是 "unknown guy")。

线框将此 URL 与我们的 MessageFeature 的路由模式相匹配,要求其创建 ViewController,并通过解释 MessageFeature 给出的 RoutingOption 来展示它。

好了,一个问候我们的弹窗就会出现。

只是为 AlertViewController 做了许多事情,但面对更大的 ViewController,它可以很好地扩展,帮助你使它们保持简单、分离,并有一个干净的架构,保持演示和域逻辑不与 ViewController 混淆,使其各就各位。

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

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

添加 Redux

剧透:如果您没有完成最后一章,您可以使用 VISPER.xcworkspace 中的 Wireframe-Example 的代码作为此教学的基础。您必须将此工作区中的 "Wireframe-Example" 部分替换为以下代码来实现这一点

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

在此之后运行 pod install

如果您对 Redux 这个概念很陌生,先从阅读以下两篇文章开始。

如果你已经了解 Redux 但并未完全理解其细节,也请考虑阅读它们😛.

完成这些后,简要查看上一章的项目并创建两个状态。

struct UserState: Equatable {
    let userName: String
}

struct AppState: Equatable {
    let userState: UserState?
}

剧透:让你的AppState 符合 Equatable 是一种可选但良好的实践,因为这使得处理状态变化变得更加容易。

AppState 应该包含我们应用程序的完整状态。由于整个应用程序的状态可能相当复杂,因此最佳实践是将其组成不同的子状态,以便以更本地化的方式处理状态复杂性。

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

下一步是替换我们的 WireframeApp 为一个 AnyVISPERApp。这为我们提供了一个 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 减少新创建的状态本身并返回结果。

剧透:我们很想为此编写程序,但由于组合的应用程序状态是一个泛型结构,没有创建此类结构的方法,所以我们陷入了困境。我们的最佳猜测是使用 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 是一个可以通知当其值发生变化时的 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 的属性。我们将使用它来订阅属性并将该订阅作为久存在 (它应该与其控制器一样长) 存储在节目中。查看 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 也已经正确行为并使用我们从我们的州中的名称。

当你尝试构建项目时,你会遇到一些错误,因为 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"}))

现在构建应用程序会在其作为其反应数据源使用应用程序状态的情况下运行应用程序。

如果您的状态没有改变,拥有一个响应式数据源是非常无聊的。是时候介绍一些状态改变了。

我们首先给我们的 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。由于我们的视图只应该关注查看内容并响应用户输入,触发状态改变应该由表示器来完成。

表示器通过将一个 Action(一个简单的消息对象)发送到 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 ...
    //
}

现在我们需要一个由 StartPresenter 触发的 Action。

struct ChangeUserNameAction: Action {
    let username: String?
}

它只是一个非常简单的结构 'carring'(有些人会说 'messaging')我们的用户名字符串到改变 appState 的 reducers。

在表示器中分发它非常简单。

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 处理当收到一个 ChangeUserNameActionUserState(以及结果 appState)的改变。

但还有希望,创建这样一个东西很容易(它可以由 Sourcery 根据需要生成)

struct ChangeUserNameReducer: ActionReducerType {

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

在添加到您的 StartFeature 后,它就完成了。

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

启动应用程序现在应该会在您输入用户名时更改按钮的标题。

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

组件

应用程序

您的 VISPER 应用程序的核心组件是 App 协议的一个实例,它允许您通过一个 Feature 来配置您的应用程序,该 Feature 代表您应用程序的一个独特功能并为它配置所有使用的 VISPER 组件。

App 协议的定义相当简单

public protocol App {

    /// Add a feature to your application
    ///
    /// - Parameter feature: your feature
    /// - Throws: throws errors thrown by your feature observers
    ///
    /// - note: A Feature is an empty protocol representing a distinct funtionality of your application.
    ///         It will be provided to all FeatureObservers after addition to configure and connect it to
    ///         your application and your remaining features. Have look at LogicFeature and LogicFeatureObserver for an example.
    func add(feature: Feature) throws

    /// Add an observer to configure your application after adding a feature.
    /// Have look at LogicFeature and LogicFeatureObserver for an example.
    ///
    /// - Parameter featureObserver: an object observing feature addition
    func add(featureObserver: FeatureObserver)

}

功能和FeatureObserver

您可以在一个FeatureObserver功能中添加一些应用

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

许多VISPER组件实现了自己的App功能FeatureObserver子类型。

VISPER-Wireframe提供以下内容:

VISPER-Redux提供以下内容:

VISPER-Swift提供VISPERApp,它结合了WireframeAppReduxApp的所有特性,并在使用VISPER-Framework构建的大多数应用中使用。

Wireframe

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

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

您可以使用DefaultWireframeAppFactory来创建默认配置的WireframeApp

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

如果您想在创建应用程序的情况下不创建Wireframe,则使用WireframeFactory

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

现在创建一个ViewFeature,它提供一个ViewController和一些路由选项,定义控制器将被如何展示,并将其提供给Wireframe。

class ExampleViewFeature: ViewFeature {

    var routePattern: String = "/exampleView"
    var priority: Int = 0

    //controller will be pushed on current active navigation controller 
    func makeOption(routeResult: RouteResult) -> RoutingOption {
        return DefaultRoutingOptionPush()
    }

    func makeController(routeResult: RouteResult) throws -> UIViewController {
        let controller = UIViewController()
        return controller
    }
}

将其添加到您的WireframeApp

let feature = ExampleFeature()
wireframeApp.add(feature: feature)

或添加到您的Wireframe

let feature = ExampleFeature()
wireframe.add(controllerProvider: feature, priority: feature.priority)
wireframe.add(optionProvider: feature, priority: feature.priority)
try wireframe.add(routePattern: feature.routePattern)

现在您可以路由到ExampleFeature提供的控制器

try wireframe.route(url: URL(string: "/exampleView")!)

以下是一个使用VISPER与wireframe的全示例

路由模式

需要文档说明,请查看JLRoutes中对路由模式的定义(我们之前曾抄袭过他们的主意)。

VISPER-Redux

VISPER-Redux是Swift中redux架构的实现。

它为您提供了一个应用程序架构来处理分布式应用程序状态和状态变化问题。它是在VISPER框架的基础上,实现了很多应用程序的交互层。

如想了解更多关于redux的信息,请查看以下教程和文档

关于VISPER-Redux的全面介绍可以在这里找到。

状态

VISPER-Redux将您的应用程序的完整状态存储在一个中央结构中,以便创建不同应用程序组件当前状态的透明表示。

一个典型的应用程序复合状态可能如下,用于管理下周待办事项:

struct AppState {
    var userState: UserState
    var todoListState: TodoListState
    var imprintState: ImprintState
}

以及一些复合子状态

    struct UserState {
    var userName: String
    var isAuthenticated: Bool
}
struct TodoListState {
    var todos: [Todo]
}
struct ImprintState {
    var text: String
    var didAlreadyRead: Bool
}

AppReducer

每个存储都有一个特殊的具有以下定义的reducer

public typealias AppReducer<State> = (_ ReducerProvider: ReducerProvider,_ action: Action, _ state: State) -> State

它用作进入商店的单个入口点。每当分发一个动作时,都会调用它来解析新的状态。由于我们的状态是通用的,因此需要将每个状态属性的创建委托给reducerProvider参数。

之前定义的AppState应该看起来像这样的AppReducer

let appReducer = { (reducerProvider: ReducerProvider, action: Action, currentState: AppState) -> AppState in
    let state = AppState(
        userState: reducerProvider.reduce(action,currentState.userState),
        todoListState: reducerProvider.reduce(action,currentState.todoListState),
        imprintState : reducerProvider.reduce(action,currentState.imprintState)
    )
    return reducerProvider.reduce(action,state)
}

ReduxApp

创建 Redux 应用非常简单

let appState: AppState = AppState( ... create your state here ...)
let factory = ReduxAppFactory()
let app: ReduxApp<AppState> = factory.makeApplication(initialState: appState, appReducer: appReducer)

更改状态

使用VISPER-Redux的app当前状态存储在一个中央Store实例中,该实例位于便利包装对象的类型Redux中。状态的更改只能通过在ActionDispatcher处分发一个动作(一个简单的消息对象)并在Reducer(reduce-function或类型为FunctionalReducerActionReducerTypeAsyncActionReducerType的实例)中创建一个修改后的新状态来实现。

reduce-function具有以下形式(其中ActionType和StateType是类型Action和Any的类型通配符)

(_ provider: ReducerProvider,_ action: ActionType, _ state: StateType) -> StateType

reduce-function/reducer将被应用于所有类型为ActionType的动作和所有类型为StateType的状态。可以通过将其添加到reducer容器中,将reducer添加到您的redux架构中。

// add a reduce function
app.redux.reducerContainer.addReduceFunction(...)
// add a action reducer instance
app.redux.reducerContainer.addReducer(...)

动作只是一个遵循空协议Action的简单对象,例如

struct SetUsernameAction: Action {
    var newUsername: String
}

let action = SetUsernameAction(newUsername: "Max Mustermann")
app.redux.actionDispatcher.dispatch(action)

Reducer

Reducers指定在将动作发送到存储时应用程序的状态如何更改。请记住,动作只描述发生了什么,但不描述应用程序的状态如何更改。VISPER swift中的reducer可以是reduce-function,或者类型为FunctionalReducerActionReducerType
AsyncActionReducerType的实例。

ReduceFuntion

ReduceFuntion函数简单地获取一个提供者、一个操作和一个状态,然后返回同一类型的新状态。

let reduceFunction = { (provider: ReducerProvider, action: SetUsernameAction, state: UserState) -> UserState in
    return UserState(userName: action.newUsername,
              isAuthenticated: state.isAuthenticated)
}
reducerContainer.addReduceFunction(reduceFunction:reduceFunction)
FunctionalReducer

FunctionalReducer与ReduceFuntion非常相似,只是它是一个接收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

async 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给您的应用添加一些reducers。

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 包含一个用于表示 AppState 变化的 ObservableProperty。 ObservableProperty 允许您订阅状态变化,并且可以映射到 RxSwift-Observable。它在 VISPER-Reactive 组件中实现。

SubscriptionReferenceBag

需要文档说明


所有可用的 VISPER 组件的完整列表

  • VISPER - 一个方便导入包装器,用于通过一个导入包含所有 VISPER 组件。它包含一些用于向后兼容先前 VISPER 版本已弃用的组件。
  • VISPER-Swift - VISPER-Framework 中所有 Swift 组件,以及所有依赖项的方便导入包装器。
  • VISPER-Objc - 一个包装器,用于在 objc 代码库中使用核心 VISPER 类。
  • VISPER-Core - 在您的特征组件之间通信时使用的某些常见核心协议。如果您想将 VISPER 组件包括到自己的项目中,应该使用此 pod。它在其他 VISPER 组件 pods 中实现了协议。
  • VISPER-Wireframe - 包含在 VIPER-Application 中线框层实现的组件,它管理 ViewControllers 的展示和生命周期。
  • VISPER-Presenter(swift / objc) - 包含在 VIPER-Application 中展示层实现的组件。它包含一些用于分离您的应用程序逻辑和视图逻辑的 presenter 类。
  • VISPER-Redux - 包含在许多 VISPER-Application 中使用的 Redux 架构实现的组件,用于表示 viper 应用程序中的 interactor 层。
  • VISPER-Reactive - 简单的反应式属性实现,允许多在 VISPER-Application 中使用反应式 Redux 架构。它可以由 subspec VISPER-Rective/RxSwift 更新,以使用 RxSwift 框架。
  • VISPER-Sourcery - 一个组件,支持您通过为您创建必要的模板代码来创建 VISPER 应用程序。
  • VISPER-UIViewController (Swift / (Objective-C) - 扩展UIViewControllers以通知视图控制器其生命周期(viewDidLoad等)的组件
  • VISPER-Entity - 如果在VISPER-Application中没有使用自定义层,则用于建模实体层的组件。