测试已测试 | ✗ |
Lang语言 | SwiftSwift |
许可证 | MIT |
发布上次发布 | 2017年10月 |
SwiftSwift 版本 | 3.0 |
SPM支持 SPM | ✗ |
由Andy Tong 维护。
AHServiceRouter 是一个轻量级的非侵入式路由器,用于执行任务和导航 VC,还具有回收功能。
AHServiceRouter 主要由两部分组成:导航 VC 和执行任务。
区分不同的导航请求和任务的方法是使用服务和任务。
“服务名称”代表一个命名空间或一个分类,“任务名称”代表一个特定的任务。
只要这些任务的名称是唯一的,一个服务就可以有多个任务。
当然,服务名称也应该在全球范围内是唯一的。
两步
/// We define an independent struct here as a key manager, it's not necessary but recommended.
struct SettingPageService {
static let service = "SettingPageService"
static let taskNavigateToVC = "taskNavigateToVC"
static let taskCreateVC = "taskCreateVC"
static let keyGetVC = "keyGetVC"
/// You must include a value for this key!
static let keyShouldRefresh = "keyShouldRefresh"
}
/// This should be somewhere in your delegate object or a manager, NOT a view controller!!
/// Use registerVC to register anything related to navigation as well as presentation.
/// We register with the key 'taskNavigateToVC'.
/// Parameter 'userInfo' is a [String: Any] passed by the service user when they use the 'navigateVC' method.
AHServiceRouter.registerVC(SettingPageService.service, taskName: SettingPageService.taskNavigateToVC) { (userInfo) -> UIViewController? in
/// We first check if the userInfo includes the key, if not, we return nil, assuming the value of 'keyShouldRefresh' is something that SettingPage must need to operate, otherwise it can't be used.
guard let shouldRefresh = userInfo[SettingPageService.keyShouldRefresh] as? Bool else{
return nil
}
let vc = SettingPage()
vc.shouldRefresh = shouldRefresh
return vc
}
/// For service users
import AHServiceRouter
import SettingPageService
/// Let's pretend we are in the mainPage now:)
/// And the following class is mainPageVC's delegate object.
/// NOTE: we use a class here, it could be a struct as well.
/// But class is the preferred choice for VC delegates.
class MainPageDelegateObject: MainPageDelegate {
func mainPageDidTapSettingButton(_ vc: MainPage) {
guard let navVC = vc.navigationController else {
return
}
/// The completion closure here is called after completing the navgation by the router. It's not related to the service provider!
AHServiceRouter.navigateVC(SettingPageService.service, taskName: SettingPageService.taskNavigateToVC, userInfo: [SettingPageService.keyShouldRefresh: true], type: .push(navVC: navVC), completion: nil)
}
}
正如您所见,userInfo 参数可以用来将值从用户传递到服务提供商。
在这种情况下,它是“true”值对应的 key“keyShouldRefresh”。
有三个导航类型
public enum AHServiceNavigationType{
case present(currentVC: UIViewController)
/// Will wrap a navVC to the target vc, presenting by currentVC
case presentWithNavVC(currentVC: UIViewController)
case push(navVC: UINavigationController)
}
/// You can do this if you support both presenting and navigating
var type: AHServiceNavigationType
if vc.navigationController != nil {
type = .push(navVC: vc.navigationController!)
}else{
type = .present(currentVC: vc)
}
作为服务提供商,您应该通过提供键(如“taskNavigateToVC”或“taskPresentVC”)(虽然应该使用“registerVC”方法进行注册并使用“navigateVC”方法进行显示)来记录您支持哪些类型的导航。
因为导航到 VC 和显示 VC 有时需要不同的 UI 设置。
让我们通过任务创建一个 VC,而不是使用内建的 navigateVC 方法进行导航。
/// Register first!
/// We register with the key 'taskCreateVC' this time.
/// Parameter 'completon' is an optional completion closure from the user in order to notify them when you finish, along with a flag and a dict. 'completion?(Bool, [String: Any])'.
/// If your task is needed to be done asynchronously, use the 'completion' to pass the results when finish, otherwise return the results directly.
/// In this case, we return the newly created vc directly in a [String : Any] -- the difference part from 'registerVC'.
AHServiceRouter.registerTask(SettingPageService.service, taskName: SettingPageService.taskCreateVC) { (userInfo, completion) -> [String : Any]? in
guard let shouldRefresh = userInfo[SettingPageService.keyShouldRefresh] as? Bool else{
completion?(false, nil)
return nil
}
let vc = SettingPage()
vc.shouldRefresh = shouldRefresh
completion(true, nil)
return [SettingPageService.keyGetVC: vc]
}
/// Now we use it
/// This method is in mainPage's delegate object
func mainPageGetSettingController(_ vc: MainPage) -> UIViewController? {
/// use the return data from the 'doTask' method. The data is '[SettingPageService.keyGetVC: vc]' shown in above code snippet.
guard let data = AHServiceRouter.doTask(SettingPageService.service, taskName: SettingPageService.taskCreateVC, userInfo: [SettingPageService.keyShouldRefresh: false], completion: nil) else {
return
}
guard let vc = data[SettingPageService.keyGetVC] as? UIViewController else {
return
}
return vc
}
/// In mainPage VC
func settingButtonDidTap(_ sender: UIButton) {
guard let settingVC = self.delegate?.mainPageGetSettingController(self) else{
return
}
/// You can ask the SettingPage guy to register a service/task to provide additional data for the animation, if you can't handle the animation yourself within your MainPage module.
vc.transitioningDelegate = someAnimator
vc.modalPresentationStyle = .custom
self.present(vc, animated: true, completion: nil)
}
这是 AHServiceRouter 进行转场动画的方式 – 我们在这里所做的一切就是将 UI 层与您的业务逻辑隔离开来 – 为维护性和可扩展性发挥更大的作用!
正如我们所知,重用业务逻辑很困难,但重用 UI 逻辑和组件是可能的。
所以如果您这样构建应用程序 – MVCs 仅执行 UI 事务,它们会向其代理请求额外信息。
那么您的 UI 逻辑肯定可以重用。更重要的是,它与业务逻辑的超弱耦合。
两个代理方法。
基本与注册服务/任务相同,但仅在 AHServiceRouter 找不到服务或任务时调用。
public protocol AHServiceRouterDelegate {
/// Do NOT do navigation yourself! Just return the fallback VC.
func fallbackVC(for service: String, task: String, userInfo: [String: Any]) -> UIViewController?
/// If your task is needed to be done asynchronously, use the 'completion' to pass the results when finish, otherwise return the results directly.
func fallbackTask(for service: String, task: String, userInfo: [String: Any], completion: AHServiceCompletion?) -> [String: Any]?
}
注:AHServiceCompletion 是 ‘(_ finished: Bool,_ userInfo: [String: Any]?) -> Void’ 的别称。
AHServiceRouter 应该作为一个基本的路由系统,它将所有独立模块和服务粘合在一起,以成为一个可维护、可扩展的 iOS 应用。
你绝对绝对不要将路由逻辑放入视图控制器中,甚至不要放入你的整个 UI 层中。
你应该总是将那些路由逻辑委托给另一个对象或管理器或另一个层。
你可能会问,有其他路由器提供的 URL 方案怎么办?
AHServiceRouter 不提供任何与 URL 方案相关的功能。
你应该自行开发一个 URL 方案解释器模块。
例如,你可以使用 'Inter-app' 作为服务名,'OAuth' 作为任务名。
每次你的应用被其他应用调用进行 OAuth 登录时,你首先让 URL 方案解释器将 URL 分解为服务名和任务名,然后使用 AHServiceRouter。
因此,如何定义你的 URL 方案的责任在于你。
AHServiceRouter 拥有的仅仅是服务名和任务名!
顺便说一下,这里的“模块”是指使用 “pod lib create” 的 Cocoapod 的 pod 模块。
有关 pod 的更多信息,请参阅
注:你可以跳过以下与产品设计相关的部分,直接学习回收视图控制器代码。
让我们假设我们有一个以下导航栈
"mainPage -> showPage -> audioPlayer -> showPage -> showPage -> audioPlayer"
每个 'showPage' 都有一个 '推荐节目' 部分。
因此,这种导航栈可以无限进行。
防止这种无限导航的解决方案是战略性地重新使用一些 VCs。
问:为什么我们不直接在导航栏左上角的“<”返回按钮旁边放一个“关闭”按钮,然后返回到根视图控制器?
答:我们可以!
因此,策略是这样的
当堆栈中已经显示了下一个展示时,我们部分重新使用 'showPage',然后跳转到那个 'showPage',而不是继续推送(创建) 'showPage' VC。
原因如下:1)我们并没有完全重用它。因为如果当前的 VC 是 'showPage',你想从这里查看另一个推荐节目,那么应该将 '推荐 showPage' 推送到堆栈中,而不是重用当前的 'showPage'。2)只有在有一个已显示的 'showPage' 时才弹出,提示用户“哦,我之前已经看过这个节目了”,这也有助于缩短堆栈,如果不是完全的话。
我们还完全重用了 audioPlayer。
原因是 'audioPlayer' 在音频应用中就像是一个目的地,在从 '推荐节目' 部分中进行长时间搜索和滚动之后。
核心方法
/// If you pass a navVC then AHServiceRouter will iterate its childViewControllers and ask you if the childViewController is the one you wnat to reuse(or recycle).
/// If you don't pass a navVC or pass a nil, AHServiceRouter will find the first UINavigationController under the keyWindow then iterate through its childViewControllers.
static func reuseVC(navigationVC: UINavigationController? = nil,_ shouldBeReused: (_ currentVC: UIViewController) -> Bool) -> UIViewController?
以下用于回收具有相同 showId 的 'showPage'。
/// Recycling policy is only defined in the service provider side when registering. The service user doesn't know anything about recycling a viewController.
AHServiceRouter.registerVC(ShowPageServices.service, taskName: ShowPageServices.taskNavigation) { (userInfo) -> UIViewController? in
/// Check if the user includes a showId which will be used in the 'reuseVC' method
guard let showId = userInfo[AHFMShowPageServices.keyShowId] as? Int else {
print("You must pass a showId into userInfo")
return nil
}
/// Here we don't pass a navVC into 'reuseVC' method.
/// So the default navVC is used and the method will iterate through the navVC's childViewControllers.
/// The default navVC is the first UINavigationController under the keyWindow.
var vc: ShowPage? = AHServiceRouter.reuseVC({ (vc) -> Bool in
/// Check if the vc is the same kind
guard vc.isKind(of: ShowPage.self), let showPage = vc as! ShowPage else {
return false
}
/// Returning true tells the method this is the one we want to reuse.
return showPage.showId == showId
})
if vc == nil {
/// There's no reusable 'showPage' in the stack
vc = ShowPage()
}
/// do the assigning showId again just in case there's a 'didSet' listener in the 'showPage' VC.
vc?.showId = showId
return vc
}
以下用于在堆栈中回收 'audioPlayer'(如果有的话)。
AHServiceRouter.registerVC(AudioPlayerVCServices.service, taskName: AudioPlayerVCServices.taskNavigation) { (userInfo) -> UIViewController? in
guard let trackId = userInfo[AudioPlayerVCServices.keyTrackId] as? Int else {
return nil
}
var vc: AudioPlayerVC? = AHServiceRouter.reuseVC({ (vc) -> Bool in
/// Check if the vc is the same kind, if it is, return true, no further checking needed.
if vc.isKind(of: AudioPlayerVC.self) else {
return true
}else{
return false
}
})
if vc == nil {
vc = AudioPlayerVC()
}
vc?.trackId = trackId
return vc
}
假设我们有两个页面 pageA 和 pageB。
当用户登录时,pageA 才会路由到 pageB。
如果没有登录,则首先展示登录 VC。
当用户完成登录后,pageA 需要传递一些数据给 pageB。
如果用户已经登录,pageA 可以直接使用 userInfo 将路由到 pageB。
这个问题的难点在于,当用户完成登录时页面A不会响应,因为登录与网络有关。
解决这个问题的主要方法是给loginVC传递一个完成闭包,然后loginVC完成时将调用它。
注意:你必须编写你的服务结构体的文档,以便你的队友知道如何使用它。
/// Define services and classes
struct PageBServices{
static let service = "\(Self.self)"
static let taskNavigateToVC = "taskNavigateToVC"
/// Int
static let keyOrderNumber = "keyOrderNumber"
/// Double
static let keyPrice = "keyPrice"
}
class PageB: UIViewController {
var orderNumber:Int?
var price: Double?
}
struct LoginVCServices{
static let service = "\(Self.self)"
/// use .presentWithNavVC to present
static let taskPresentVC = "taskPresentVC"
/// the value should be a closure of form '(_ succeeded: Bool)->Void'
/// this key-value is optional!
static let keyCompletionClosure = "keyCompletionClosure"
}
class LoginVC: UIViewController {
var completion: ((_ succeeded: Bool)->Void)?
func dismissButtonTapped(_ sender: UIButton) {
/// invoke the completion closure when dismiss, with a failed login status
completion?(false)
}
/// Called when finish authentication
func handleClosure(_ isSucceeded: Bool) {
completion?(isSucceeded)
}
}
/// Register first!!
/// Register PageBServices
AHServiceRouter.registerVC(PageBServices.service, taskName: PageBServices.taskNavigation) { (userInfo) -> UIViewController? in
guard let orderNumber = userInfo[PageBServices.keyOrderNumber] as? Int,
let price = userInfo[PageBServices.keyPrice] as? Double else {
return nil
}
let vc = PageB()
vc.orderNumber = orderNumber
vc.price = price
return vc
}
/// Register LoginVCServices
AHServiceRouter.registerVC(LoginVCServices.service, taskName: LoginVCServices.taskNavigation) { (userInfo) -> UIViewController? in
let vc = LoginVC()
/// the completion closure is optional parameter.
if let comletion = userInfo[LoginVCServices.keyCompletionClosure] as? ((_ succeeded: Bool)->Void) else {
vc.comletion = comletion
}
return vc
}
/// Now let's pretend we are in pageA's delegate class.
import AHServiceRouter
import PageBServices
/// You have to import the actual module, not just the service module.
import PageB
import LoginVCServices
/// You have to import the actual module, not just the service module.
import LoginVC
class PageADelegateObject: NSObject, PageADelegate {
func comfirmButtonDidTap(_ vc: PageB, orderNumber: Int, price: Double) {
guard let navVC = vc.navigationController else {return}
if checkLogin() {
/// route to pageB directly with a infoDict required by its services.
navigateToPageB(orderNumber, price)
}else{
/// user not loggd in !!!
let completion: (_ succeeded: Bool)->Void = { (succeeded) in
if succeeded {
self.navigateToPageB(orderNumber, price)
}else{
// error handling here, it might be a network error
}
}
let closureDict = [LoginVCServices.keyCompletionClosure: completion]
AHServiceRouter.navigateVC(LoginVCServices.service, taskName: LoginVCServices.taskPresentVC, userInfo: closureDict, type: .presentWithNavVC(currentVC: vc), completion: nil)
}
}
func navigateToPageB(_ orderNumber: Int, _ price: Double) {
/// Make sure the infoDict has the correct info for routing to pageB
let infoDict = [PageBServices.keyOrderNumber: orderNumber, PageBServices.keyPrice: price]
AHServiceRouter.navigateVC(PageBServices.service, taskName: PageBServices.taskNavigateToVC, userInfo: infoDict, type: .push(navVC: navVC), completion: nil)
}
func checkLogin() -> Bool {
/// .... logics to decide if the current user is logged in or not .... it could be another service provided by the LoginVCServices.
}
}
问:在‘registerTask’中有一个完成调用。这个完成调用能否在注册LoginVC时使用?
/// Register this way
AHServiceRouter.registerTask(LoginVCServices.service, taskName: LoginVCServices.taskCreateVC) { (userInfo, completion) -> [String : Any]? in
let vc = LoginVC()
/// instead of invoking the comletion here, assigning it to the vc.
vc.completion = completion
/// You need to return a dict when regisgter tasks!
return [LoginVCServices.keyGetVC: vc]
}
/// Use it
func navigateToPageB(_ pageA: PageA) {
let completionClosure: ((_ succeeded: Bool, info: [String : Any]?)) = { (succeeded,info) in
if succeeded {
// navigate to pageB here
}
}
/// pass 'completionClosure' to 'doTask' method
guard let data = AHServiceRouter.doTask(LoginVCServices.service, taskName: LoginVCServices.taskCreateVC, userInfo: [:], completion: completionClosure) else {
return
}
guard let vc = data[LoginVCServices.keyGetVC] as? UIViewController else {
return
}
// present vc here
}
答:没错!你必须编写文档,并让大家知道在调用该完成调用时。
当你有一个异步任务,你不能立即返回结果时,你可以保持这个完成调用,稍后调用它。
但是,当你可以使其清晰,显然这是第一种方法,那么就让它清晰吧!
示例项目是空的。
AHServiceRouter可以通过CocoaPods获得。要安装它,只需将以下行添加到您的Podfile中
pod "AHServiceRouter"
Andy Tong, [email protected]
AHServiceRouter在MIT许可下可用。有关更多信息,请参阅LICENSE文件。