VISPER-UIViewController 5.0.0

VISPER-UIViewController 5.0.0

Jan Bartel 维护。



 
依赖
VISPER-Objc~> 5.0.0
VISPER-Presenter~> 5.0.0
 

VISPER

Version License Platform

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

使用 VISPER 构建的典型应用的架构如下图中所示。

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

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

Architecture

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

最常见的组件是 App 协议,该协议帮助您将您的应用组合为分离的模块,称为 功能。每个功能都创建并配置了您应用的一个独立部分。

由于视图控制器(ViewController)的展示、创建和管理是您应用的重要部分,因此应该有一个单独的组件来处理视图控制器的展示和生命周期。这项工作由 Wireframe 完成。

Wireframe 允许您通过简单的 URL(称为 Route)路由到一个特定的 ViewController。它将 ViewController 的创建(由 ViewFeature 完成)与其展示(由一些叫作 RoutingPresenter 的家伙完成)分开。

由于我们和家人为庞大的 ViewController 打了太多的仗,我们希望我们的视图非常愚蠢。这为 Presenter 的情况做好了准备。Wireframe 在展示 ViewController 之前,从一个 PresenterFeature 获取所有负责的 Presenters。它给予他们机会,在实际上展示之前,通过一些数据绑定和行为丰富那个愚蠢的东西。

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

您可以通过向 ActionDispatcher 提交 Action 在展示器中触发状态更改,同时在另一边观察状态变化来更新您的视图。关于这个话题的更多信息可以在 这里 找到。

索引

入门

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

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

线框图入门

首先创建一个使用 UINavigationController 的简单项目。

剧透:VISPER可以管理您选择的任何容器视图控制器,但由于默认实现中已经预配置了UINavigationController,因此很容易从中开始。

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

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var visperApp: WireframeApp!

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

DefaultWireframeAppFactory创建一个已配置用于与 UINavigationController 一起使用的 WireframeApp。有关 Wireframe 的更多信息,可以在此处找到,但让我们先完成 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 已经初始化并准备好路由,但它没有要创建的视图控制器,因此让我们创建一个 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)
    }
    
}

好吧,不要抱怨,这是一个非常愚蠢的控制器,它甚至都不美观...但有时,你所需要的只是一个可以四处乱逛并愿意帮助的人...

所以让我们 Just 使用它并将其放入 ViewFeature。

警告:ViewFeature是来自"VISPER_Wireframe"库的协议,必须实现以向_wireframe提供ViewController。我们在VISPER-Pod中定义了一些typealiases,以便使用"import VISPER"语句。这是一个不太高明的技巧,但它允许你在我们的子库中的VISPER类或协议中始终使用"import VISPER"语句。

import VISPER

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

正如你所看到的,必须实现两种方法和一个属性来提供ViewController给Wireframe。

让我们从routePattern开始。它是一个String,用来描述匹配此控制器使用的路由。

这意味着 routePattern var routePattern: String = "/start" 将与url var url = URL(string: "/start") 相匹配...开始很简单,但最终变得复杂...routePatterns可以包含变量、通配符和其他内容(你可以在路由模式部分中找到更多信息),我们暂时假设我们完全理解它们的工作原理😬 .

下一个方法非常直接,makeController(routeResult: RouteResult) -> UIViewController 函数只需创建在 wireframe.route(url: URL('/start')!) 在我们的应用程序中的任何地方调用时要显示的viewController(假设我们的routePattern是"/start")。

routeResult-Parameter 包含一些有用的上下文信息,当创建ViewController时可能会用到,尽管在这里我们不需要它(稍后查看路由结果部分)😉 ).

makeOption(routeResult:) 比较复杂(当需要时查看路由选项部分)。它定义了Wireframe默认如何显示ViewController。返回 DefaultRoutingOptionPush 将导致Wireframe将该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,他不能处理除了视图之外的其他任何东西...

我们需要的是连接这个愚蠢视图与我们的交互者和wireframe层中单技能马的某人。我们通过展示者来做到这一点。

展示者协议相当简单

protocol Presenter {
    func isResponsible(routeResult: RouteResult, controller: UIViewController) -> Bool
    func addPresentationLogic( routeResult: RouteResult, controller: UIViewController) throws
}

让我们创建一个可以配置我们的控制器的展示者。

class StartPresenter: Presenter {
    
    var userName: String
    
    init(userName: String) {
        self.userName = userName
    }
    
    func isResponsible(routeResult: RouteResult, controller: UIViewController) -> Bool {
        return controller is StartViewController
    }
    
    func addPresentationLogic(routeResult: RouteResult, controller: UIViewController) throws {
        
        guard let controller = controller as? StartViewController else {
            fatalError("needs a StartViewController")
        }
        
        controller.buttonTitle = "Hello \(self.userName)"
        
        controller.tapEvent = { _ in
            print("Nice to meet you \(self.userName)!")
        }
        
    }
}

演讲者负责类 StartViewController 的 ViewController,向按钮标题添加一些信息,并为按钮的点击事件添加一些行为。

假设我们的演讲者从它的环境中获得了用户名的信息作为依赖项,我们将在后续的交互器/Redux 层中添加该信息。

要向我们的应用程序添加演讲者,我们只需要将新的协议 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 代码作为本教程的起点。您需要将此工作区的 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 应该包含我们应用的全状态。由于整个应用的状态可能会相当复杂,最好将其组成不同的子状态来以更本地化的方式克服状态复杂性。

尽管我们的状态并不真正复杂,但在这种情况下开始是个好主意,以展示如果需要,如何进行状态组合。

下一步应该将我们的 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 减少新创建的状态,并返回结果。

剧透:我们非常乐意为您编程,但由于组合的 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 属性的类型更改为 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 也能正确行为并使用我们从 AppState 中获取的名字。

当您尝试构建项目时,您会得到一些错误,因为 StartPresenter 的构造函数已更改。它想要 'ObservableProperty' 参数现在。

让我们通过向 StartFeature 和您的 StartPresenter 注入这样一个属性来改变这种情况。

class StartFeature: ViewFeature {
    
    let routePattern: String
    
    let priority: Int = 0
    let wireframe: Wireframe
    let userName: ObservableProperty<String>
    
    init(routePattern: String, wireframe: Wireframe,userName: ObservableProperty<String>){
        self.routePattern = routePattern
        self.wireframe = wireframe
        self.userName = userName
    }
    
    // imagine the ViewFeature functions here
}


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

我们将使用映射,在您的 AppDelegate 中注入它到 StartFeature

let startFeature = StartFeature(routePattern: "/start",
                                   wireframe: visperApp.wireframe,
                                    userName: visperApp.redux.store.observableState.map({ return $0.userState.userName ?? "Unknown Person"}))

现在构建应用程序将生成一个使用 appstate 作为其响应式数据源的运行应用程序。

如果您不改变状态,响应式数据源相当无聊。是时候引入一些状态变化了。

我们首先在我们的 StartViewController 中添加一个输入字段。

class StartViewController: UIViewController, UITextFieldDelegate {
    
    typealias ButtonTap = (_ sender: UIButton) -> Void
    typealias NameChanged = (_ sender: UITextField, _ username: String?) -> Void
    
    weak var button: UIButton! {
        didSet {
            self.button?.setTitle(self.buttonTitle, for: .normal)
        }
    }
    
    weak var nameField: UITextField?
    
    var buttonTitle: String? {
        didSet {
            self.button?.setTitle(self.buttonTitle, for: .normal)
        }
    }

    var tapEvent: ButtonTap?
    var nameChanged: NameChanged?
    
    override func loadView() {
        
        let view = UIView()
        self.view = view
        
        let nameField = UITextField(frame: .null)
        nameField.translatesAutoresizingMaskIntoConstraints = false
        self.nameField = nameField
        self.navigationItem.titleView = nameField
        nameField.placeholder = "enter your username here"
        nameField.addTarget(self, action: #selector(textFieldChanged), for: .editingChanged)
        nameField.backgroundColor = .white
        
        let button = UIButton()
        self.button = button
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(self.tapped(sender:)), for: .touchUpInside)
        
        self.view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.topAnchor.constraint(equalTo: self.view.topAnchor),
            button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
            button.leftAnchor.constraint(equalTo: self.view.leftAnchor),
            button.rightAnchor.constraint(equalTo: self.view.rightAnchor)
            ])
     
    }
    
    @objc func tapped(sender: UIButton) {
        self.tapEvent?(sender)
    }
    
    @objc func textFieldChanged(textField: UITextField) {
        self.nameChanged?(textField, textField.text)
    }
    
}

这里没有什么神奇的事情发生,它只是我们常见的 UIViewController,其中包含一个按钮和一个在标题视图中的 UITextField。现在我们想要在文本框中输入用户名时更改我们的 appstate。由于我们的视图应该只关注查看内容和响应用户输入,触发状态变化应该在主讲人那里完成。

主讲人通过向 ActionDispatcher(更多信息请参阅 此处)发送一个 Action(一个简单的消息对象)来触发状态变化。所以让我们给他提供这样一个东西。

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

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

由于我们现在需要一个 ActionDispatcher 来初始化主讲人,让我们也把我们的 StartFeature 给它。

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

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

并在 AppDelegate 中注入它。

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

现在我们需要一个 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))
    }
}

您现在可以构建您的应用程序,但如果您输入用户名,appstate 并不改变。

这是怎么回事呢?

好吧,当接受一个 ChangeUserNameAction 时,没有任何 Reducer 处理 UserState(以及作为结果的 AppState)的变化。

但还有希望,这样的事情很容易构建(它甚至可以被 Sourcery 生成)。

struct ChangeUserNameReducer: ActionReducerType {

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

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

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

现在启动您的应用程序应该导致在您输入用户名时改变按钮的标题的响应式应用程序。

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

组件

应用程序

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

App 协议的定义相当简单

public protocol App {

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

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

}

功能和功能观察器

您基本上可以向一个功能观察器功能中添加一些。

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

许多VISPER组件实现了它们自己的App子类型、功能功能观察器

VISPER-Wireframe为您提供

VISPER-Redux为您提供

VISPER-Swift提供给您一个VISPERApp,该软件组合了WireframeAppReduxApp的所有特性,并用于大多数使用VISPER框架构建的应用程序。

Wireframe

Wireframe负责管理VISPER应用程序中UIControl挫的生命周期。它用于创建控制器并将路由从一个控制器切换到另一个。

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

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

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

如果您想创建一个不创建WireframeApp的Wireframe,则使用WireframeFactory.

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

现在创建一个ViewFeature,该软件提供一个ViewController和一些路由选项,以定义如何显示该控制器。

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 和线框图的全示例

Routepatterns

需要被文档化,请查看 JLRoutes 中路由图的定义(我们早已从这里借用了这个想法)。

VISPER-Redux

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

它为您提供了一种应用架构来处理分布式应用状态和状态变化的问题。它是许多基于 VISPER 框架的应用中交互层实现。

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

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

State

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

对于一个应用来说,通常的组合应用状态可能如下所示,以管理下周todos

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

它用作存储的唯一入口点。每当有 action 分发时,它都会被调用,以解决新的状态。由于我们的状态是通用的,因此有必要将每个状态属性的创建委托给 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 函数或 FunctionalReducerActionReducerTypeAsyncActionReducerType 的实例)中创建一个修改后的新状态。

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 的实例。
AsyncActionReducerType

ReduceFuntion

归约函数仅仅是一种简单的函数,它接收一个提供者、一个操作和一个状态,并返回相同类型的新状态。

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

功能性归约器与归约器非常相似,它只需要接收一个归约函数并使用它来归约状态。

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

操作类型归约器是一个包含特定操作和状态类型的归约函数的ActionReducerType类。

struct SetUsernameReducer: ActionReducerType {

    typealias ReducerStateType  = UserState
    typealias ReducerActionType = SetUsernameAction

    func reduce(provider: ReducerProvider,
                  action: SetUsernameAction,
                   state: UserState) -> UserState {
                   
                   return UserState(userName: action.newUsername,
                             isAuthenticated: state.isAuthenticated)
    }
}

let reducer = SetUsernameReducer()
reducerContainer.addReducer(reducer:reducer)
AsyncActionReducerType

异步归约器是AsyncActionReducerType类型的一种归约器,它不返回新状态,而是调用一个带有新状态的完成函数。

struct SetUsernameReducer: AsyncActionReducer {

    typealias ReducerStateType  = UserState
    typealias ReducerActionType = SetUsernameAction

    let currentState: ObserveableProperty<UserState>

    func reduce(provider: ReducerProvider,
                  action: SetUsernameAction,
              completion: @escaping (_ newState: UserState) -> Void) {
        let newState =  UserState(userName: action.newUsername,
        isAuthenticated: self.currentState.value.isAuthenticated)
        completion(newState)
    }
}


let reducer = SetUsernameReducer(currentState: app.state.map{ $0.userState })
reducerContainer.addReducer(reducer:reducer)

LogicFeature

您可以使用LogicFeature向应用程序添加一些归约器。

import VISPER_Redux

class ExampleLogicFeature: LogicFeature {

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

}


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

观察状态变化

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

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


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

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

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

SubscriptionReferenceBag

需要文档记录


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

  • VISPER - 一个便利的导入包装器,用于通过一个导入包含所有 VISPER 组件。它包含一些过时的组件以确保向后兼容先前的 VISPER 版本。
  • VISPER-Swift - VISPER-Framework 中的所有 Swift 组件,以及它们的依赖的便利导入包装器。
  • VISPER-Objc - 一个封装在核心 VISPER 类的包装器,用于在 objc 代码库中使用它们。
  • VISPER-Core - 一些在您的功能组件之间进行通信的常用核心协议。如果您想在您的项目中包含 VISPER 组件和组件,应使用此 pod。它实现的协议在其他 VISPER 组件路由 pod 中。
  • VISPER-Wireframe - 包含 VIPER-应用程序中线框层实现组件,它管理 ViewController 的显示和生命周期。
  • VISPER-Presenter(swift / objc) - 包含 VIPER-应用程序中呈现层实现组件。它包含一些呈现器类以将您的应用程序逻辑与视图逻辑分开。
  • VISPER-Redux - 包含用于在 Vikings 应用程序中表征交互层所使用的 redux 架构组件。
  • VISPER-Reactive - 对响应式属性的简单实现,允许在 VISPER-应用程序中使用响应式 redux 架构。它可以通过 subspec VISPER-Rective/RxSwift 更新以使用 RxSwift 框架。
  • VISPER-Sourcery - 一个支持通过为您创建一些必要的样板代码来创建 VISPER 应用的组件。
  • VISPER-UIViewController (swift) / (objc) - 一个扩展 UIViewControllers 以通知呈现器关于其生命周期的组件( viewDidLoad 等)
  • VISPER-Entity - 如果您在 VISPER-应用程序中不使用自己的层,则模拟实体层的组件。