理解中的路由应该包含路由页面跳转管理、通过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进行操作, 如
goNamed
,viewController(name...)
方法; why:
当路由路径比较复杂,且含有参数的时候, 如果通过硬编码的方法直接手写路径, 可能会造成拼写错误,参数位置错误等错误
在实际app中, 路由的
URL
格式可能会随着时间而改变, 但是一般路由名称不会去更改
通过路由路径获取对应的控制器:
let vc = try await FJRouter.jump().viewController(byLocation: "/five", extra: nil)
通过路由名称获取对应的控制器:
let fvc = try await FJRouter.jump().viewController(byName: "five", params: [:], queryParams: [:], extra: nil)
通过路由路径导航至对应控制器:
try FJRouter.jump().go(location: "/second", extra: nil, from: self, ignoreError: true)
通过路由名称导航至对应控制器:
try FJRouter.jump().goNamed("second", params: [:], queryParams: [:], extra: nil, from: self, ignoreError: true)
3: 支持路由回调
路由回调是使用
Combine
框架实现的
只支持跳转的使用使用
async
异步, 且返回值是AnyPublisher<FJRouter.CallbackItem, FJRouter.MatchError>
的go
方法
不需要提前注册回调方法, 只需要在收到
Combine
事件流中区分对应的事件
let callback = await FJRouter.jump().goNamed(FJRouter.GoByNameParams.init(name: "second"))
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
1: 此参数是block
形式的类型别名
public typealias Animator = (@MainActor @Sendable (_ info: AnimatorInfo) -> any FJRouteAnimator)
2: 用于在使用go
、goNamed
方法进行跳转时, 页面的展现方式。
3: 可以使用框架已内置的动画方式对象, 或者可以使用自己实现的协议对象, 具体参考FJRouteAnimator。
4: 没有push
和present
的概念; 所有的动画细节均隐藏在协议对象里:
FJRoute.SystemPushAnimator
对应push
FJRoute.SystemPresentAnimator
对应present
1: 有些路由地址需要拦截器,例如对于没有登录的用户,有些页面就无法访问.eg:
let route = try FJRoute(path: "/", name: "root", builder: nil, redirect: FJRouteCommonRedirector(redirect: { state in
switch AppUser.shared.hasLogin {
case true:
if let app = await UIApplication.shared.versionkKeyWindow?.rootViewController, app is AppTabBarViewController {
return nil // 返回nil代表不需要进行重定向
}
return "/app"
case false:
if let app = await UIApplication.shared.versionkKeyWindow?.rootViewController, !(app is AppTabBarViewController) {
return nil // 返回nil代表不需要进行重定向
}
return try? await FJRouter.shared.convertLocationBy(name: "loginAccount")
}
}))
2: 此参数可以为nil
, 但是为nil
时, 构建控制器参数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 startAnimatedTransitioning(from fromVC: UIViewController?, to toVC: 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.RefreshSamePreviousAnimator
try 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路径
/// 指向需要重定向的路由路径。返回`nil`, 则代表不需要重定向
func redirectRoute(state: FJRouterState) async throws -> String?
2: 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()
}))
}
跳转
try? FJRouter.jump().goNamed(FJRouter.GoByNameParams.init(name: "first"))
let callback = await FJRouter.jump().goNamed(FJRouter.GoByNameParams.init(name: "second"))
callback.sink(receiveCompletion: { cop in
print("cop----全部", cop)
}, receiveValue: { item in
print("value----全部", item)
}).store(in: &cancels)
try? FJRouter.jump().go(location: FJRouter.GoByLocationParams.init(location: "/six"))
1: 资源管理是定义为FJRouterResourceable
的协议, 可以通过FJRouter.resource()
获取框架资源管理中心对象.
2: 建议使用try FJRouterResource(path: "/xxx", name: "xxx", value: xxx), get(name: xxx)方法进行相关操作。
当资源路径比较复杂,且含有参数的时候, 如果通过硬编码的方法直接手写路径, 可能会造成拼写错误,参数位置错误等错误
在实际app中, 资源的
URL
格式可能会随着时间而改变, 但是一般资源名称不会去更改
func put(_ resource: FJRouterResource) async throws
1: 资源可以是int, string, enum, uiview, uiviewcontroller, protocol...
2: 资源必须是Sendable
修饰的对象
3: 存放资源的时候可以携带参数
4: 适用于全局只会存放一次的资源: 如单例中或者application:didFinishLaunchingWithOptions
中, 或者存放的资源内部具体的值是个固定值, 不会随着时间/操作更改
5: 如果每次存放资源可能会更改, 建议使用put(_ resource: FJRouterResource, uniquingPathWith: xxx)
方法
6: 事例代码:
let r1 = try FJRouterResource(path: "/intvalue1", name: "intvalue1", value: { _ in 1 })
try await FJRouter.resource().put(r1)
let r3 = try FJRouterResource(path: "/intOptionalvalue2", name: "intOptionalvalue2") { @Sendable info -> Int? in
return nil
}
try await FJRouter.resource().put(r3)
let r5 = try FJRouterResource(path: "/intOptionalvalue3/:optional", name: "intOptionalvalue3", value: { @Sendable info -> Int? in
let isOptional = info.pathParameters["optional"] == "1"
return isOptional ? nil : 1
})
try await FJRouter.resource().put(r5)
存放协议
let r6 = try FJRouterResource(path: "/protocolATest/:isA", name: "protocolATest", value: { @Sendable info -> ATestable in
let isA = info.pathParameters["isA"] == "1"
return isA ? AModel() : BModel()
})
try await FJRouter.resource().put(r6)
func put(_ resource: FJRouterResource, uniquingPathWith combine: @Sendable (_ current: @escaping FJRouterResource.Value, _ new: @escaping FJRouterResource.Value) -> FJRouterResource.Value) async
1: 如果已经存放过相同path
的资源, 不会抛出FJRouter.PutResourceError.exist
错误, 会按照combine
策略进行合并
2: 适用于可能多处/多处存放: 如某个viewController, 出现的时候才去存储资源, 但是因为viewController可能会多次进入, 而且每次存放的资源的具体值均不相同, 使用此方法可以有效的存储, 不会抛出FJRouter.PutResourceError.exist
错误
3: 资源的名称会优先使用新的资源name, 如果新的资源name为nil, 才会使用旧资源name
4: 事例代码
// 使用旧值
await FJRouter.resource().put(r) { (currnet, _) in currnet }
// 使用新值
await FJRouter.resource().put(r) { (_, new) in new }
1: 可以通过路径和资源名称获取资源
func get<Value>(_ location: String, inMainActor mainActor: Bool) async throws -> Value where Value: Sendable
func get<Value>(name: String, params: [String : String], queryParams: [String : String], inMainActor mainActor: Bool) async throws -> Value where Value: Sendable
2: 事例代码
let intvalue1: Int = try await FJRouter.resource().get("/intvalue1", inMainActor: false)
let intvalue3: Int? = try await FJRouter.resource().get("/intvalue1", inMainActor: true)
let intOptionalvalue3: Int? = try await FJRouter.resource().get("/intOptionalvalue2", inMainActor: false)
let stringvalue1: String = try await FJRouter.resource().get("/stringvalue1", inMainActor: false)
let aTestable1: ATestable = try await FJRouter.resource().get("/protocolATest/1", inMainActor: false)
let aTestable2: ATestable = try await FJRouter.resource().get("/protocolATest/0", inMainActor: true)
let aTestable3: BModel = try await FJRouter.resource().get("/protocolATest/0", inMainActor: false)
1: 可以通过路径和资源名称更新资源
func update(byPath path: String, value: @escaping FJRouterResource.Value) async throws
func update(byName name: String, value: @escaping FJRouterResource.Value) async throws
2: 如果没有存放过相同path的资源, 会抛出FJRouter.GetResourceError.notFind
错误
3: 事例代码
try await impl.update(byName: "sintvalue1", value: { _ in 66 })
try await FJRouter.resource().delete(byPath: "adfasdf")
func delete(byPath path: String) async throws
func delete(byName name: String) async throws
1: 必须是已经存放过的资源, 删除不存在的资源会抛出FJRouter.GetResourceError.notFind
错误
2: 可以根据名称或者路由删除
3: 事例代码:
try await FJRouter.resource().delete(byPath: "/intvalue1")
try await FJRouter.resource().delete(byName: "intOptionalvalue1")
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: "onSeekSuccess", params: [:], queryParams: [:], extra: 5)
// 有参
try await FJRouter.event().emit(name: "onSeekProgress", params: ["progress": "1"], queryParams: [:], 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
。