TEXT-VALID-VISPER-Wireframe 5.0.0

VISPER-Wireframe 5.0.0

Jan Bartel维护。



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

VISPER

Version License Platform

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

下面展示了使用VISPER构建的典型应用程序的架构。

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

我们为此主题创建了一篇单独的文章进行深入探讨。您可以在这里找到:这里

Architecture

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

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

由于展示、创建和管理ViewController是应用程序的重要组成部分,因此应该有一个单独的组件负责展示和ViewController的生命周期。这项工作由Wireframe完成。

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

鉴于我们与其他大型ViewController打了很多仗,我们想让我们的视图相当愚蠢。这为Presenter奠定了舞台。Wireframe在展示ViewController之前,从PresenterFeature请求所有负责的Presenters。它给了他们可能的机会,在它实际上被展示之前,用一些数据绑定和行为来丰富这个东西。

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

您可以通过向ActionDispatcher提交Action在呈现器中触发状态更改,并在另一边观察状态更改以更新视图。更多关于这个主题的信息可以在这里找到。

索引

入门

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

我们将在下一步扩展这个示例,添加一些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。如果您想了解更多关于线框图的信息,可以在这里找到这个信息,但让我们暂时完成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"-Pod的一个协议,必须实现以向wireframe提供ViewController。我们在VISPER-Pod中定义了一些typealiases,以便能够为它提供一个"import VISPER"语句。这是一个 dirty trick,但是它允许你在我们的子pod中的任何一个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()
    }
}

正如您可能看到的,需要实现两个方法和一个属性来为wireframe提供ViewController。

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

这意味着routePattern var routePattern: String = "/start"将由URL var url = URL(string: "/start")匹配......开始很简单,但最后很复杂......routePatterns可以包含变量、通配符和其他内容(您可以在RoutePattern部分中了解更多信息),在这种情况下,我们假设我们对它们的工作方式有完整的了解。😬 .

下一个方法相当简单,makeController(routeResult: RouteResult) -> UIViewController函数应该只是创建当wireframe.route(url: URL('/start')!)在应用程序的任何地方被调用时要展示的viewController(假设我们的routePattern是"/start")。

routeResult-Parameter包含一些创建ViewController时可能有用的上下文信息,尽管在这里我们不需要它(稍后请参阅RouteResult部分)😉 ).

makeOption(routeResult:)比前面复杂一些(如有需要,请参阅RoutingOption部分)。它定义了wireframe默认方式下如何展示ViewController。返回一个DefaultRoutingOptionPush会导致wireframe将我们的ViewController推向其当前的最顶端viewController。

很容易😇,让我们在我们的应用程序中注册我们的新功能......回到您的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。但那很棒😊,让我们添加一些功能😻.

添加一个演示者(Presenter)

管理应用程序逻辑需要一位了解其环境的聪明人,因此这显然不是我们的愚蠢的ViewController,他除了视图之外无法处理任何东西...

我们需要的是将愚蠢的视图与我们互动层和wireframe层的单一技巧马连接起来的人。我们可以通过一个Presenter来实现这一点。

Presenter-Protocol相当简单

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

那么让我们创建一个能够配置我们控制器的Presenter。

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)!")
        }
        
    }
}

Presenter负责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 呈现代码Dialog作为模态视图控制器呈现。

在实现所有这些之后,现在是时候将新的功能添加到 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

SPOILER:如果您在上一个章节中没有完成,您可以将 VISPER.xcworkspace 中的 Wireframe-Example 代码用作本教程的起点。您必须将工作区中的 "Wireframe-Example" 部分替换为以下代码来实现这一点

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

之后运行 pod install

如果您对 Redux 是一个新概念,请先阅读以下两篇文章。

如果您已经了解 Redux 但并未完全理解其所有细节,那么阅读它们也很好😛.

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

struct UserState: Equatable {
    let userName: String
}

struct AppState: Equatable {
    let userState: UserState?
}

剧透:让您的 AppState 符合 Equatable 是一个非必要的但良好的实践,因为这使得处理状态变化更加容易。

AppState 应该包含我们应用的完整状态。由于整个应用的状态可能相当复杂,因此最佳实践是以不同的子状态来组合它,以便以更本地化的方式克服状态复杂性。

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

下一步应该是用 AnyVISPERApp 替换我们的 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 是一个通用的结构体,并且没有动态创建此类结构体的方法,所以我们在这里遇到了问题。我们最好的猜测是使用 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)]
    }
    
}

我们将使用 map 注入它在您的 AppDelegate 中。

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

现在构建应用程序将导致一个运行的应用程序,它使用 appstate 作为其反应式数据源。

嗯,如果您状态不改变,那么拥有一个反应式数据源是非常无聊的。现在是引入一些状态变化的时候了。

我们首先给 StartViewController 添加一个输入字段

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

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

这里没有发生任何神奇的事情,这只是我们普通的 UIViewController,其中包含了一个按钮和一个 UITextField。现在,如果你想在这个文本字段中输入用户名,我们要改变我们的应用状态。由于我们的视图只需要关注显示内容和响应用户输入,触发状态改变应该在展示者(presenter)中进行。

展示者通过向ActionDispatcher(更多信息请参阅这里)分派一个动作(一个简单的消息对象)来触发状态改变。所以,让我们提供这样的事物。

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?
}

这只是一个非常简单的结构,承载着我们的用户名字符串到改变应用程序状态的减法器(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))
    }
}

你现在可以构建你的应用程序了,但如果你输入用户名,应用程序状态并没有改变。

那这是怎么回事?

好吧,没有减法器处理在接收ChangeUserNameAction时的UserState(以及结果AppState)的变化。

但是有希望,构建这样的事情很简单(它甚至可以通过Sourcery生成)。

struct ChangeUserNameReducer: ActionReducerType {

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

在添加到你的StartFeature中后,就完成了。

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

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

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

组件

应用程序

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

App协议的定义相当简单。

public protocol App {

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

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

}

特性和FeatureObserver

您基本上可以向应用程序中添加一些 特征观察者功能

每当添加一个 特征观察者 时,将会调用该观察者,它负责配置您的 VISPER 组件以提供您的 功能 所实现的功能。

许多 VISPER 组件实现了他们自己 应用程序功能特征观察者 的子类型。

VISPER-Wireframe 为您提供:

VISPER-Redux 为您提供:

VISPER-Swift 提供了一个 VISPERApp,它结合了 WireframeAppReduxApp 所有的特性,并且通常用于构建使用 VISPER-Framework 的应用程序。

Wireframe

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

Wireframe 组件包含了 Wireframe 协议的实现。

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

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

如果您想创建 Wireframe 而不想创建 WireframeApp,请使用 WireframeFactory

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

现在创建一个 ViewFeature,该功能提供 ViewController 和一些路由选项,以定义控制器将如何由 Wireframe 展示。

class ExampleViewFeature: ViewFeature {

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

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

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

将其添加到您的 WireframeApp

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

或您的 Wireframe

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

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

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

这里是一个使用 Wireframe 的 VISPER 的完整示例

路由模式

需要被记录,查看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 的应用程序中,当前状态存储在中心 Store 实例中,它位于类型为 Redux 的便捷包装对象中。状态更改只能通过在 ActionDispatcher 上分派一个操作(一个简单的消息对象)以及在 Reducer( reductions 函数或类型 FunctionalReducerActionReducerTypeAsyncActionReducerType 的实例)中创建修改后的新状态来实现。

reductions 函数有以下形式(其中 ActionType 和 StateType 是类型为 Action 和 Any 的泛型类型)

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

reductions 函数 / 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 可以是 reductions 函数,也可以是类型 FunctionalReducerActionReducerTypeAsyncActionReducerType 的实例。

ReduceFuntion

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

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

功能性reducer相当类似,它只接收一个reducer函数并用它来减少状态。

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

动作类型reducer是ActionReducerType中的一个类,它包含特定动作和状态类型的reducer函数。

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向您的应用程序添加一些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)

观察状态变化

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

//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,用于表示与应用程序状态的变化。该ObservableProperty 允许您订阅状态变化,并可以映射到 RxSwift-Observable。它在VISPER-Reactive 组件中实现。

SubscriptionReferenceBag

需要编写文档


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

  • VISPER - 一个方便的导入包装器,用于通过一个导入包括所有共享组件。它包含一些为了 backward compatibility 包含了旧 VISPER 版本中的过时组件。
  • VISPER-Swift - VISPER-Framework 中的所有 Swift 组件,以及所有依赖项的方便导入包装器。
  • VISPER-Objc - 帮助在 objc 代码库中使用的核心 VISPER 类的包装器。
  • VISPER-Core - 一组常用的核心协议,用于在您功能的不同组件之间进行通信。如果您想要将 VISPER 组件包含到自己的项目中,应当使用这个 pod。它的协议在其他 VISPER 组件的 pod 中实现。
  • VISPER-Wireframe - 包含实现 VIPER-应用程序中 wireframe 层的组件,它管理 ViewControllers 的呈现和生命周期。
  • VISPER-Presenter(swift / objc) - 包含 VIPER-应用程序中呈现层实现的组件。包含一些 presenter 类,以将您的应用逻辑与视图逻辑分离。
  • VISPER-Redux - 包含 Redux 架构实现的组件,广泛用于许多 VISPER-应用程序以表示 viper 应用程序中的 interactor 层。
  • VISPER-Reactive - 使用反应式属性的一个简单实现,允许在 VISPER-应用程序中使用反应式 Redux 架构。它可以通过 SUBSPEC VISPER-Rective/RxSwift 来更新,以使用 RxSwift 框架。
  • VISPER-Sourcery - 一个组件,它可以帮助您通过为您生成需要的 boilerplate 代码来创建一个 VISPER 应用程序。
  • VISPER-UIViewController (Swift / (Objective-C) - 扩展UIViewControllers的一个组件,用于通知主持人其生命周期(viewDidLoad等)
  • VISPER-Entity - 如果在VISPER-Application中不使用自定义层,则用于模拟实体层的组件。