VISPER-Objc 5.0.2

VISPER-Objc 5.0.2

Jan Bartel维护。



 
依赖关系
VISPER-Core~> 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 打下了基础。在展示 ViewController 之前,wireframe 从 PresenterFeature 请求所有负责的 Presenters。它允许他们在实际展示之前用一些数据绑定和行为丰富这个简单的东西。

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

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

索引

入门

由于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 创建了一个 WireframeApp,它已经配置好了用于与 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。所以让我们创建一个😅),它不需要了解 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。我们已经在VISPER-Pod中定义了一些类型别名,以便通过“import VISPER”语句提供它。这是一个不太干净的技巧,但它允许你即使是对于我们的一些子Pod中的类或协议,也可以使用“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。

我们先从routePattern开始。它是一个描述用于匹配此控制器的路由的字符串。

这意味着routePattern var routePattern: String = "/start"将通过URL var url = URL(string: "/start")进行匹配...很简单,但最终却很复杂... 路径模式可以包含变量、占位符以及其他东西(你可以在RoutePattern部分找到更多关于它们的信息),暂时假设我们已经完全了解了它们是如何工作的。😬 .

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

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

makeOption(routeResult:)要复杂一些(如有需要,请查看RoutingOption部分)。它定义了视图框架默认应该如何呈现ViewController。返回DefaultRoutingOptionPush将导致视图框架仅将我们的viewController推送到它的当前 UINavigationController。

好吧,这个很简单😇,让我们在我们的应用程序中注册我们的新特性... 返回您的AppDelegate,并添加它

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
    //just imagine we created and initialized your visperApp here
    
     let startFeature = StartFeature(routePattern: "/start")
     try! visperApp.add(feature: startFeature)
        
}

现在visperApp将为我们注册“/start”路由的特征,这使我们能够通过另一行代码用它进行路由

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
    //just imagine we created and initialized your visperApp here
    
     let startFeature = StartFeature(routePattern: "/start")
     try! visperApp.add(feature: startFeature)
     
     try! visperApp.wireframe.route(url: URL(string: "/start")!)
     
     self.window?.makeKeyAndVisible()
}

现在您的AppDelegate的完整代码应该像这样

import UIKit
import VISPER

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var visperApp: WireframeApp!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        let window = UIWindow(frame: UIScreen.main.bounds)
        self.window = window
        
        let factory = DefaultWireframeAppFactory()
        let visperApp = factory.makeApp()
        self.visperApp = visperApp
        
        let navigationController = UINavigationController()
        window.rootViewController = navigationController
        visperApp.navigateOn(navigationController)
        
        let startFeature = StartFeature(routePattern: "/start")
        try! visperApp.add(feature: startFeature)
    
        try! visperApp.wireframe.route(url: URL(string: "/start")!)
        
        self.window?.makeKeyAndVisible()
        return true
    }

}

如果您启动应用程序,“唰”的一声... 您将看到一个完全没有用的丑陋的黑色ViewController,它带有一个居中的UIButton。但这很好😊,让我们添加一些功能😻.

添加一个展示器

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

我们需要的是将愚蠢的视图与我们交互器层和视图框架层中的一技之长连接起来的人。我们通过展示器(Presenter)来获得它。

展示器协议相当简单

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,为按钮标题添加了一些信息,并为按钮的点击事件添加了一些行为。

假设我们的演示者从其环境中获取用户名信息作为依赖项,我们将在interactor/redux层中稍后添加该信息。

要将演示者添加到我们的应用程序中,我们只需要将我们的《StartFeature》扩展到新的协议《PresenterFeature》中。

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

并从我们的《StartFeature》中删除以下令人讨厌的行(应该创建组件,添加数据是演示者的工作)

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

现在点击视图按钮将导致 Debug 输出 "很高兴认识你,未知之友!",但是如何以一个包含此消息的警告信息来呈现会更好呢?

接受挑战吗?

让我们开始创建一个新的功能,创建一个《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》并不真正知道它从哪里获取用户名吗?这个问题的原因是这个例子不包含实际的interactor层。我们将通过在下一章向这个例子添加Redux架构来解决此问题。

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

添加Redux

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

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

之后,运行pod install

如果Redux是一个新概念,请从阅读以下两篇文章开始。

如果你已经了解Redux但并未完全理解它的所有细节,也可以考虑阅读它们😛.

完成阅读后,简要回顾一下上一章节的项目,并创建两个状态。

struct UserState: Equatable {
    let userName: String
}

struct AppState: Equatable {
    let userState: UserState?
}

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

AppState应该包含我们应用的全部状态。由于整个应用的状态可能会变得相当复杂,所以最佳实践是将它分解为不同的子状态,以便更本地化地解决状态复杂性。

虽然我们的状态并不复杂,但以这种方式开始是一个好主意,以展示如果需要的话,状态组合应该如何进行。

下一步应该是用AnyVISPERApp替换我们的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的属性。我们将使用它来订阅该属性并将该订阅存储在presenter存在期间(应该就像它所在的控制器存在一样)。有关这方面的更多信息,请参阅SubscriptionReferenceBag部分。

func addPresentationLogic(routeResult: RouteResult, controller: UIViewController) throws {
    
   guard let controller = controller as? StartViewController else {
       fatalError("needs a StartViewController")
   }
    
   let subscription = self.userName.subscribe { (value) in
               controller.buttonTitle = "Hello \(value)"
           }
   self.referenceBag.addReference(reference: subscription)
    
   controller.tapEvent = { [weak self] (_) in
        guard let presenter = self else { return }
        let path = "/message/\(presenter.userName.value)".addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
        let url = URL(string:path)
        try! presenter.wireframe.route(url: url!)
   }
}

因此,当用户名发生变化时,按钮标题将相应地更改。由于我们在tapEvent中从更改为,即使MessageFeature也行为正确并使用我们的AppState中的名称。

当你尝试构建项目时,由于StartPresenter的构造函数已更改,你会得到一些错误。现在,它需要一个`ObservableProperty`参数。

让我们通过向StartFeatureStartPresenter中注入这样的属性来修改这一点。

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


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

我们将在AppDelegate中使用了map将其注入到StartFeature

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

现在构建应用将导致使用应用状态作为其响应式数据源的运行应用程序。

拥有一个响应式数据源在您的状态未发生变化时非常无聊。是时候引入一些状态变化了。

我们首先在我们的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,其中包含一个按钮和title视图中一个UITextField。现在如果我们输入用户名到文本字段中,我们想要改变我们的应用状态。由于我们的视图应该只关注查看内容和响应用户输入,触发状态变化应该在演示者中完成。

演示者通过向操作(一个简单的消息对象)发送到一个操作派发器(了解更多关于它的这里)来触发状态变化。所以让我们给他这样一件东西

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
}

由于现在我们需要一个操作派发器来初始化演示者,让我们把它也给我们的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?
}

它只是一个非常简单的结构,承载(有些人会说“携带”)我们的用户名字符串到更改应用状态的reducer。

在演示者中派发它很容易

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

现在您可以构建您应用了,但如果您输入用户名,应用状态不会改变。

那么这到底是什么呢?

嗯,没有reducer处理在接收到ChangeUserNameActionUserState(以及结果的应用状态)的变化。

但是,有希望,构建这样一件东西很简单(它甚至可以被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应用的核心组件是应用协议的一个实例,该协议允许您通过一个特性来配置您的应用,该特性代表您应用的独特功能,并配置所有使用的VISPER组件。

应用协议的定义相当简单

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 组件实现了自己子类 应用功能特征观察者

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)

如果您想创建一个线框图而不创建线框应用程序,请使用线框工厂

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

现在创建一个ViewFeature,它提供了一个ViewController和一些RoutingOptions,以定义如何向线框展示控制器。

class ExampleViewFeature: ViewFeature {

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

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

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

将其添加到您的线框应用程序

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

或您的线框图

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

路由模式

需要文档,请参阅JLRoutes中对路由模式的定义(我们从前偷了一些想法)。

VISPER-Redux

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

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

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

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

状态

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

一个典型的复合应用程序状态,以便一个应用程序在下周管理您的待办事项,可能看起来像这样

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

具有一些复合子状态

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

AppReducer

每个存储都有一个具有以下定义的特殊还原器

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

它用作存储的单个入口点。每当分发一个动作时,就会调用它以解决新的状态。由于我们的状态是通用的,因此有必要将每个状态属性的创建委托给还原器提供者参数。

一个适用于前面定义的 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

创建一个 redis 配件非常简单

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 上分发一个操作(一个简单的消息对象),并在还原器(一个 reduce 函数或类型 FunctionalReducerActionReducerTypeAsyncActionReducerType) 中创建一个修改后的新状态,才能实现状态变更。

reduce 函数的形式如下(其中 ActionType 和 StateType 是类型 Action 和 Any 的泛型类型)

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

reduce 函数/还原器将应用于所有类型为 ActionType 的动作和所有类型为 StateType 的状态。可以通过将其添加到还原器容器中向您的 redix 架构添加一个还原器。

// 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负责指定应用状态如何对发送到仓库的动作做出响应。请记住,动作仅描述发生了什么,但不描述应用状态如何变化。在VISPER swift中的Reducer可以是reduce-function,或者FunctionalReducer的实例,ActionReducerType
或者AsyncActionReducerType

ReduceFuntion

reduce funtion只是一个获取提供者、动作和状态,并返回类型相同的新的状态的简单函数。

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

功能Reducer非常相似,只是它接受一个reduce函数并使用它来减少状态。

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

动作类型Reducer是包含特定动作和状态类型的reduce函数的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

异步Reducer是AsyncActionReducerType的Reducer,它不返回新的状态,而是调用一个带有新状态的完成项。

struct SetUsernameReducer: AsyncActionReducer {

    typealias ReducerStateType  = UserState
    typealias ReducerActionType = SetUsernameAction

    let currentState: ObserveableProperty<UserState>

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


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

LogicFeature

您可以使用LogicFeature将一些Reducer添加到您的应用程序中。

import VISPER_Redux

class ExampleLogicFeature: LogicFeature {

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

}


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

观察状态变化

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

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


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

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

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

SubscriptionReferenceBag

需要被记录文档


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

  • VISPER - 一个便捷的导入包装器,可使用单个导入包含所有VISPER组件。它包含一些已弃用的组件,以保持与先前VISPER版本的向后兼容。
  • VISPER-Swift - VISPER框架的所有swift组件,以及它们的依赖的便捷导入包装器。
  • VISPER-Objc - 一个包装在核心VISPER类周围的封装,可在objc代码库中使用。
  • VISPER-Core - 一些常用的核心协议,用于在特征的不同组件之间进行通信。如果您想将VISPER组件包含到自己的项目中,应使用该pod,其协议在其他的VISPER组件pods中实现。
  • VISPER-Wireframe - 包含在VIPER应用程序中实现wireframe层的组件,它管理ViewController的表示和生命周期。
  • VISPER-Presenter(swift / objc) - 包含在VIPER应用程序中实现presentation层的组件。它包含一些presenter类,用于将应用程序逻辑与视图逻辑分离。
  • VISPER-Redux - 包含在许多VISPER应用程序中使用来表示VIPER应用程序中interactor层的redux架构的组件。
  • VISPER-Reactive - 一个简单的响应式属性的实现,允许在VISPER-Application中使用响应式Redux架构。可以通过VISPER-Rective/RxSwift subspec进行更新,以便使用RxSwift框架。
  • VISPER-Sourcery - 一个组件,通过为您创建一些必要的样板代码,帮助您构建VISPER应用。
  • VISPER-UIViewController (swift) / (objc) - 一个扩展UIViewControllers的组件,用于通知presenter有关其生命周期(viewDidLoad等)。
  • VISPER-Entity - 在您不使用自定义层的情况下,用于建模层组件。