路由包含路由页面跳转管理、通过url获取对应位置的资源
框架包含三大模块:
通过提前注册url pattern规则/path/:{路径参数}, 通过正则表达式解析并匹配其中的所有参数。eg:
-
单个参数: 注册路径
/users/:userId, 解析出此url路径的参数数组为[userId], 可以匹配/users/..., 如/users/123, 可以匹配成功并解析出userId为123; -
多个参数: 注册路径
/city/school/:sname/class/:cid, 解析出此url路径的参数数组为[sname, cid], 可以匹配/city/school/xxx/class/xxx, 如/city/school/清华大学/class/123, 可以匹配成功并解析出参数为["sname": "清华大学", "cid": "123"]; -
无参数: 注册路径
/settings/detail, 可以匹配/settings/detail,以及携带查询参数的url, 如/settings/detail?q=a、/settings/detail?p=1&q=2...
1: 路由页面跳转是定义为FJRouterJumpable的协议, 可以通过FJRouter.jump()获取框架内跳转管理中心对象.
2: 支持通过路由路径和已经注册的路由名称进行对应操作
当然在这里, 建议通过路由名称相关的api进行操作, 如
go(.name(...)),viewController(.name(...))方法; why:
当路由路径比较复杂,且含有参数的时候, 如果通过硬编码的方法直接手写路径, 可能会造成拼写错误,参数位置错误等错误
在实际app中, 路由的
URL格式可能会随着时间而改变, 但是一般路由名称不会去更改
通过路由路径获取对应的控制器:
let vc = try await FJRouter.jump().viewController(.loc("/"), extra: nil)
通过路由名称获取对应的控制器:
let vc2 = try await FJRouter.jump().viewController(.name("root", params: ["id": "123"]), extra: nil)
通过路由路径导航至对应控制器:
try await FJRouter.jump().go(.loc("/"), extra: nil, from: self, ignoreError: true)
通过路由名称导航至对应控制器:
try await FJRouter.jump().go(.name("root"), extra: nil, from: self, ignoreError: true)3: 支持路由回调
路由回调是使用
Combine框架实现的
不需要提前注册回调方法, 只需要在收到
Combine事件流中区分对应的事件
let callback = try await FJRouter.jump().go(.name("root"), extra: nil, from: self, ignoreError: true)
callback.sink(receiveCompletion: { cop in
print("cop----全部", cop)
}, receiveValue: { item in
print("value----全部", item)
}).store(in: &cancels)
触发:需要viewController方调用
try? dispatchFJRouterCallBack(name: "haha", value: ())
dismiss(animated: true, completion: { [weak self] in
try? self?.dispatchFJRouterCallBack(name: "completion", value: 123)
})每一个路由都通过
FJRoute对象进行配置.
1: 如果是起始父路由, 其path必须以/为前缀
2: 支持路径参数, 路由参数将被解析并储存在JJRouterState中, 用于builder和redirect
如果赋值, 必须提供唯一的字符串名称, 且不能为空
1: 此参数是block形式的类型别名
public typealias Builder = (@MainActor @Sendable (_ info: BuilderInfo) -> UIViewController)2: 此参数可以为nil, 但是为nil时, 重定向参数redirect不能为nil
3: builder可以根据路由信息BuilderInfo返回对应的控制器.
1: 此参数是block形式的类型别名
public typealias Animator = (@MainActor @Sendable (_ info: AnimatorInfo) -> any FJRouteAnimator)2: 用于在使用go(...)方法进行跳转时, 页面的展现方式。
3: 可以使用框架已内置的动画方式对象, 或者可以使用自己实现的协议对象, 具体参考FJRouteAnimator。
4: 没有push和present的概念; 所有的动画细节均隐藏在协议对象里:
FJRoute.SystemPushAnimator对应push
FJRoute.SystemPresentAnimator对应present
1: 有些路由地址需要拦截器,例如对于没有登录的用户,有些页面就无法访问.eg:
let loginRoute = try! FJRoute(path: "/login", name: "login", builder: { info in
return UIViewController()
}, redirect: [FJRouteCommonRedirector(redirect: { state in
let hasLogin = xxx
if hasLogin { // true, 即代表已经登录, 此时允许可以跳转至login路由
return .pass
}
// hasLogin: false, 即代表未登录, 此时页面在未登录相关的页面, 如登录/注册/发送验证码...等页面, 此时不允许跳转至login路由, 防止多重的跳转至登录
return .interception
})])2: 此参数是个数组, 可以添加多个, 按顺序检查; 比如:登录检查, 用户权限检查......多个条件重定向逻辑可以分开写;职能单一, 方便测试
3: 此参数可以为空, 但是为空时, 构建控制器参数builder不能为nil
1: 所谓子路由就是: 一个大的路由页面下面的多个相关联的路由。如设置页面下的设置项a,b,c,d...
2: 子路由还一个好处是, 可以减少拼写相同的path路径。如
设置项:FJRoute(path: "/user/settings", name: "user_settings", xxx)
设置项a:FJRoute(path: "/user/settings/a", name: "user_settings_a", xxx)
设置项b:FJRoute(path: "/user/settings/b", name: "user_settings_a", xxx)
设置项c:FJRoute(path: "/user/settings/c", name: "user_settings_c", xxx)
...
上述所有路由可以总结为一个设置项路由:
try FJRoute(path: "/user/settings", name: "user_settings", xxx, routes: [
FJRoute(path: "a", name: "user_settings_a", xxx),
FJRoute(path: "b", name: "user_settings_b", xxx),
FJRoute(path: "c", name: "user_settings_c", xxx),
...
])3: 子路由也支持此当前路由的参数
4: 强烈建议子路由的path不要以/为开头
let route = try FJRoute(path: "/play/:id", builder: ({ _ in ViewControllerPlay() }), routes: [
FJRoute(path: "feature1", builder: ({ _ in ViewControllerPlay1() })),
FJRoute(path: "feature2", name: "bfeature2", builder: ({ _ in ViewControllerPlay2() })),
FJRoute(path: "feature3/:name", builder: ({ _ in ViewControllerPlay3() })),
FJRoute(path: "feature3", name: "bfeature3", builder: ({ _ in ViewControllerPlay4() })),
FJRoute(path: "feature3", name: "bfeature3-1", builder: ({ _ in ViewControllerPlay5() })),
FJRoute(path: "feature4/:name", name: "feature4", builder: ({ _ in ViewControllerPlay6() })),
])1: 此协议只有一个方法
/// 开始路由转场动画
/// - Parameters:
/// - fromVC: 要跳转到的源控制器
/// - toVC: 匹配到的路由指向的控制器
/// - matchState: 匹配到的路由信息
@MainActor func startAnimated(from fromVC: UIViewController?, to toVC: @escaping @MainActor () -> UIViewController, state matchState: FJRouterState)2: 内置动画显示方式:
以
FJRoute.xxxxAnimator为开头的实例对象
-
设置app window的rootViewController:
FJRoute.AppRootControllerAnimator -
系统present动画进行显示:
FJRoute.SystemPresentAnimator -
系统push进行显示:
FJRoute.SystemPushAnimator -
根据情况自动选择动画方式:
FJRoute.AutomaticAnimator -
自定义
custom的present转场动画:FJRoute.CustomPresentationAnimator -
自定义转场动画进行push:
FJRoute.CustomPushAnimator -
自定义
fullScreen的present转场动画:FJRoute.FullScreenPresentAnimator -
系统push/pop动画风格的present/dismiss转场动画, 支持侧滑dismiss:
FJRoute.PresentSameAsPushAnimator -
刷新与匹配控制器相同类型的上一个控制器动画:
FJRoute.RefreshSamePreviousAnimatortry await FJRouter.shared.registerRoute(FJRoute(path: "/four", name: "four", builder: { sourceController, state in FourViewController() }, animator: { info in if let pvc = info.fromVC, pvc is FourViewController { // 或者其它判断条件 return FJRoute.RefreshSamePreviousAnimator { @Sendable previousVC, state in previousVC.view.backgroundColor = .random() previousVC.updatexxxx() } } return FJRoute.SystemPushAnimator() }))
1: 此协议只有一个方法: 根据匹配状态进行判断返回对应路由的url路径
/// 重定向行为: interception: 不可以跳转, 即路由守卫/pass: 不需要重定向/new(xxx)需要重定向到新路由路径: 如果返回的是`nil`, 也不需要重定向
func redirectRouteNext(state: FJRouterState) async -> FJRouteRedirectorNext2: app已经内置了一个通用的重定向: FJRouteCommonRedirector
3: 路由循环重定向, 框架内部在进行路由匹配的时候, 如果检测到巡航重定向, 则会抛出错误。如:
a->b->c->d->a
4: 最大重定向次数: 框架内部在进行单个路由匹配的时候, 会记录重定向次数, 如果次数大于设定的值, 会抛出错误。默认最大重定向次数为5, 可以通过如下代码进行设置修改:
await FJRouter.jump().setRedirectLimit(50)注册
static func register() async throws {
try await FJRouter.jump().registerRoute(FJRoute(path: "/", name: "root", builder: { @MainActor @Sendable _ in ViewController()}, animator: { info in
FJRoute.AutomaticAnimator(navigationController: UINavigationController())
}))
try await FJRouter.jump().registerRoute(FJRoute(path: "/first", name: "first", builder: { _ in FViewController() }, animator: { _ in FJRoute.SystemPushAnimator() }))
try await FJRouter.jump().registerRoute(FJRoute(path: "/second", name: "second", builder: { _ in SViewController() }, animator: { _ in
FJRoute.CustomPresentationAnimator(navigationController: UINavigationController())
}))
try await FJRouter.jump().registerRoute(FJRoute(path: "/third", name: "third", builder: { _ in TViewController() }, animator: { _ in FJRoute.SystemPresentAnimator(fullScreen: true, navigationController: UINavigationController()) }))
try await FJRouter.jump().registerRoute(FJRoute(path: "/four", name: "four", builder: { _ in FourViewController() }, animator: { info in
if let pvc = info.fromVC, pvc is FourViewController {
return FJRoute.RefreshSamePreviousAnimator { @Sendable previousVC, state in
previousVC.view.backgroundColor = .random()
}
}
return FJRoute.SystemPushAnimator()
}))
try await FJRouter.jump().registerRoute(FJRoute(path: "/five", name: "five", builder: { _ in FiveViewController() }, animator: { _ in FJRoute. CustomPresentationAnimator(navigationController: UINavigationController()) { @Sendable ctx in
ctx.usingBottomPresentation()
}}))
try await FJRouter.jump().registerRoute(FJRoute(path: "/six", name: "six", builder: { _ in SixViewController() }, animator: { info in
return FJRoute.PresentSameAsPushAnimator(navigationController: UINavigationController())
}))
try await FJRouter.jump().registerRoute(FJRoute(path: "/seven", name: "seven", builder: { _ in SevenViewController() }, animator: { info in
return FJRoute.PresentSameAsPushAnimator(navigationController: UINavigationController())
}))
try await FJRouter.jump().registerRoute(FJRoute(path: "/eight", name: "eight", builder: { _ in EightViewController() }, animator: { info in
return FJRoute.AutomaticAnimator()
}))
try await FJRouter.jump().registerRoute(FJRoute(path: "/nine", name: "nine", builder: { _ in NineViewController() }, animator: { info in
return FJRoute.SystemPushAnimator()
}))
}跳转
FJRouter.jump().go(.name("first"))
let callback = await FJRouter.jump().go(.name("second"), extra: nil, from: self, ignoreError: true)
callback.sink(receiveCompletion: { cop in
print("cop----全部", cop)
}, receiveValue: { item in
print("value----全部", item)
}).store(in: &cancels)
FJRouter.jump().go(.loc("/six"))1: 事件总线协议定义为FJRouterEventable的协议, 可以通过FJRouter.event()获取事件总线管理对象.
2: 建议使用onReceive(path: "xxx", name: "xxx"), emit(name: xxx)方法进行相关操作。
当事件路径比较复杂,且含有参数的时候, 如果通过硬编码的方法直接手写路径, 可能会造成拼写错误,参数位置错误等错误
在实际app中, 事件的
URL格式可能会随着时间而改变, 但是一般事件名称不会去更改
1: 监听动作是通过系统Combine框架进行响应, 不持有监听者
2: 可以一对多的进行监听, 即可以在多处监听
3: 事例代码
无参
let seekSuccess = try await FJRouter.event().onReceive(path: "/seek/success", name: "onSeekSuccess")
seekSuccess.receive(on: OperationQueue.main)
.sink(receiveValue: { info in
print("onSeekSuccess=>", info)
}).store(in: &self.cancels)
有参
let seekProgress = try await FJRouter.event().onReceive(path: "/seek/:progress", name: "onSeekProgress")
seekProgress.receive(on: OperationQueue.main)
.sink(receiveValue: { info in
print("onSeekProgress=>", info)
}).store(in: &self.cancels)1: 可以通过事件路径和名称进行触发
2: 事例代码
通过事件url路径触发事件
//无参
try await FJRouter.event().emit("/seek/success", extra: 5)
//有参: 1就是监听"/seek/:progress"中的progress字段
try await FJRouter.event().emit("/seek/1", extra: nil)
通过事件名称触发事件
//无参
try await FJRouter.event().emit(.name("onSeekProgress"), extra: nil)
// 有参
try await FJRouter.event().emit(.name("onSeekProgress", params: ["progress": "1"]), extra: nil)从2.0.2分支开始, 要求swfitVersion>=6, 即必须使用xcode 16.0版本以上
使用 Swift PM 的最简单的方式是找到 Project Setting -> Swift Packages
搜索
https://github.com/zgjff/FJRouter并添加
在 Podfile 文件中添加 FJRouter:
pod 'FJRouter'然后运行 pod install。