包括便捷扩展、路由、轮播图、转场动画的框架
=================
功能 | 名称 |
---|---|
路由 | JJRouter |
Toast | JJToast |
轮播图 | JJCarouselView |
转场动画 | JJTransition |
API扩展 | Extensions |
简单好用、支持block回调、转发、拦截功能的路由框架
最基本思路: 自己负责自己的显示方式
即从A跳转到B,由B自己来决定是push还是present,亦或者使用自定义的转场动画来显示
所以路由界面需要遵守JJRouterDestination
协议并实现func showDetail(withMatchRouterResult result: JJRouter.MatchResult, from sourceController: UIViewController)
方法
eg:
extension SystemPushController: JJRouterDestination {
func showDetail(withMatchRouterResult result: JJRouter.MatchResult, from sourceController: UIViewController) {
// push
sourceController.navigationController?.pushViewController(self, animated: true)
// present
sourceController.present(self, animated: true)
// 自定义转场动画--提供使present/dismiss动画跟系统push/pop动画一致的转场动画
let navi = UINavigationController(rootViewController: self)
navi.modalPresentationStyle = .fullScreen
navi.transitioningDelegate = pushPopStylePresentDelegate
sourceController.present(navi, animated: true)
// 自定义转场动画---居中弹窗
let pd = AlertPresentationController(show: self, from: sourceController) { ctx in
ctx.usingBlurBelowCoverAnimators(style: .regular)
}
transitioningDelegate = pd
sourceController.present(self, animated: true) {
let _ = pd
}
}
}
enum SimpleRouter: String, CaseIterable {
case systemPush = "/app/systemPush"
...
}
extension SimpleRouter: JJRouterSource {
var routerPattern: String {
return rawValue
}
func makeRouterDestination(parameters: [String : String], context: Any?) -> JJRouterDestination {
switch self {
case .systemPush:
return SystemPushController()
...
}
}
}
SimpleRouter.allCases.forEach { try! $0.register() }
(try? JJRouter.default.open(SimpleRouter.systemPush))?.jump(from: self)
(try? JJRouter.default.open("/app/systemPush"))?.jump(from: self)
if let url = URL(string: "https://www.appwebsite.com/app/systemPush/") {
(try? JJRouter.default.open(url))?.jump(from: self)
}
// 注册
enum PassParameterRouter {
case byEnum(p: String, q: Int)
...
}
var routerParameters: [String : String] {
switch self {
case let .byEnum(p: p, q: q):
return ["p": p, "q": "\(q)"]
...
}
}
// A
(try? JJRouter.default.open(PassParameterRouter.byContext, context: 12))?.jump(from: self)
// 参数: ["p": "entry", "q": 12]
// 注册
enum PassParameterRouter {
case byUrl = "/app/passParameterByUrl/:pid/:name"
...
}
// A
(try? JJRouter.default.open("/app/passParameterByUrl/12/jack"))?.jump(from: self)
// 参数: ["pid": "12", "name": "jack"]
// 注册
enum PassParameterRouter {
case byUrlWithQuery = "/app/search"
...
}
// A
(try? JJRouter.default.open("/app/search?name=lili&age=18"))?.jump(from: self)
// 参数: ["name": "lili", "age": "18"]
// 注册
enum PassParameterRouter {
case byContext = "/app/passParameterByContext"
...
}
// A
(try? JJRouter.default.open(PassParameterRouter.byContext, context: 12))?.jump(from: self)(self)
// B
func showDetail(withMatchRouterResult result: JJRouter.MatchResult, from sourceController: UIViewController) {
if let pid = result.context as? Int {
self.pid = pid
}
sourceController.navigationController?.pushViewController(self, animated: true)
}
// 注册
enum PassParameterRouter {
case mixUrlAndContext = "/app/mixUrlAndContext/:pid/:text"
...
}
// A
(try? JJRouter.default.open("/app/mixUrlAndContext/12/keke", context: arc4random_uniform(2) == 0))?.jump(from: self)
// 注册
enum PassParameterRouter {
case parameterForInit = "/app/parameterForInit/:id"
...
}
func makeRouterDestination(parameters: [String : String], context: Any?) -> JJRouterDestination {
switch self {
case .parameterForInit:
let idstr = parameters["id"] ?? ""
let numberFormatter = NumberFormatter()
let id = numberFormatter.number(from: idstr)?.intValue
return PassParametersForInitController(id: id ?? 0)
...
}
}
// A
(try? JJRouter.default.open("/app/parameterForInit/66"))?.jump(from: self)
// B
init(id: Int) {
pid = id
super.init(nibName: nil, bundle: nil)
}
// A
// 不缩写的代码逻辑应该是这样的
let result = try? JJRouter.default.open(BlockRouter.backBlock)
let router = result?.jump(from: self)
// 当然也可以缩写
// let router = (try? JJRouter.default.open(BlockRouter.backBlock))?.jump(from: self)
router?.register(blockName: "onSend", callback: { obj in
print("get data: \(obj) from router block")
})
// B
dismiss(animated: true) { [weak self] in
self?.router?.perform(blockName: "onSend", withObject: 5)
}
主要用于,A的数据是实时变化的,B需要拿到A的最新数据
// A
let router = (try? JJRouter.default.open(BlockRouter.frontBlockB))?.jump(from: self)
router?.register(blockName: "onNeedGetNewestData", callback: { [weak self] obj in
guard let self = self,
let block = obj as? (Int) -> () else {
return
}
block(self.data)
})
// B
let block: (Int) -> () = { [weak self] data in
self?.button.setTitle("\(data)", for: [])
}
router?.perform(blockName: "onNeedGetNewestData", withObject: block)
这里A虽然是调用B的路由,但是仍然可以收到C的回调
// register 转发
func register() throws {
try JJRouter.default.register(pattern: routerPattern, mapRouter: { matchResult in
guard case .mapBlock = self else {
return self
}
let needGotoLoginController = arc4random_uniform(2) == 0
if needGotoLoginController { // 需要登录,转发给登录路由
return SimpleRouter.login
}
return self
})
}
// A
let router = (try? JJRouter.default.open(BlockRouter.mapBlock))?.jump(from: self)
router?.register(blockName: "loginSuccess", callback: { _ in
print("登录成功")
})
/// 匹配到的路由跟当前展示的界面相同时的操作
public enum MatchedSameRouterDestinationAction {
/// 不做任何操作
case none
/// 更新数据
case update
/// 展示新界面
case new
}
/// 当匹配到的路由跟当前展示的界面相同时的操作方法,默认返回`new`
///
/// 返回`none`时,不做任何操作
///
/// 返回`update`时,会调用`updateWhenRouterIdentifierIsSame`方法来更新当前界面
///
/// 返回`new`时,会调用`showDetail`来重新展示新的界面
/// - Parameter result: 匹配结果
func actionWhenMatchedRouterDestinationSameToCurrent(withNewMatchRouterResult result: JJRouter.MatchResult) -> JJRouter.MatchedSameRouterDestinationAction {
return .update
}
func updateWhenRouterIdentifierIsSame(withNewMatchRouterResult result: JJRouter.MatchResult) {
pid = parseId(from: result.parameters)
title = "\(pid)"
}
泛型、可扩展、样式丰富多样、支持自定义显示样式
toast
分拆为样式加显示容器; 样式、显示容器高度抽象化,框架只在底层做相关的逻辑; 具体实现交由上层处理
此协议统筹约束所有的样式组件内容,并且跟样式容器协议交互(容器也只跟此层交互): 此协议约定了一共5项内容, 具体如下:
/// `toast`样式组件协议
public protocol JJToastItemable: AnyObject {
associatedtype Options: JJToastItemOptions
/// toast样式组件代理
var delegate: JJToastableDelegate? { get set }
/// 配置
var options: Options { get }
/// 唯一标识符
var identifier: String { get }
/// 使用对应的`toast`样式配置以及要显示`toast`的view的size大小, 计算并布局`toast`样式
/// - Parameters:
/// - options: 配置
/// - size: 要显示`toast`的view的size大小
func layoutToastView(with options: Options, inViewSize size: CGSize)
/// 根据显示`toast`的view的size大小重置`toast`样式size
/// - Parameter size: 显示`toast`的view的size
func resetContentSizeWithViewSize(_ size: CGSize)
}
- 文字:
JJTextToastItemable
public protocol JJTextToastItemable: JJToastItemable {
/// 展示文字内容
/// - Parameters:
/// - text: 内容
/// - labelToShow: label
func display(text: NSAttributedString, in labelToShow: UILabel)
}
- 指示器:
JJIndicatorToastItemable
/// 显示指示器的 `toast`样式组件协议
public protocol JJIndicatorToastItemable: JJToastItemable {
/// 开始动画
func startAnimating()
}
- 进度条:
JJProgressToastItemable
/// 显示进度条的 `toast`样式组件协议
public protocol JJProgressToastItemable: JJToastItemable {
/// 设置进度条进度
/// - Parameters:
/// - progress: 进度
/// - flag: 是否开启动画
func setProgress(_ progress: Float, animated flag: Bool)
}
框架提供了一种独特的toast
样式具体实现: JJMixTwoToastItem
, 它可以很方便的提供混合组合, 文字+文字、文字+指示器、文字+进度条、文字+图像、指示器+进度条、指示器+指示器。。。。
它是一个泛型类:
JJMixTwoToastItem<First: JJToastItemable, Second: JJToastItemable>
所以你可以随意的组合任意两种样式 eg
1: 文字+文字
JJMixTwoToastItem(first: JJTextToastItem(attributedString: NSAttributedString(string: "标题", attributes: [.font: UIFont.systemFont(ofSize: 22), .foregroundColor: UIColor.jRandom()])), second: JJTextToastItem(text: "我是内容我是内容我是内容我是内容我是内容我是内容我是内容我是内容我是内容我是内容"))
2: 指示器+文字
JJMixTwoToastItem(first: JJActivityToastItem(), second: JJTextToastItem(text: "我是内容我是内容我是内容我是内容我是内容我是内容我是内容我是内容我是内容我是内容"))
3: 指示器+变换的文字
JJMixTwoToastItem(first: JJActivityToastItem(), second: JJVaryTextToastItem(texts: ["加载中", "加载中.", "加载中..", "加载中..."]))
4: 指示器+指示器
JJMixTwoToastItem(first: JJArcrotationToastItem(), second: JJActivityToastItem())
5: 文字+图像
JJMixTwoToastItem(first: JJTextToastItem(text: "进击的象🐘"), second: JJImageToastItem(url: url, display: { url, imageView in
imageView.sd_setImage(with: url, completed: nil)
}))
....
此协议统筹约束所有的容器逻辑: 此协议约定了一共10项内容, 具体如下:
/// `toast`容器协议
public protocol JJToastContainer: UIView, JJToastableDelegate, CAAnimationDelegate {
/// 配置
var options: JJToastContainerOptions { get set }
/// 状态
var state: JJToastState { get set }
/// 具体承载的`toast`样式
var toastItem: (any JJToastItemable)? { get }
/// 显示toast
func present(_ viewToShow: UIView, animated flag: Bool)
/// 隐藏toast
func dismiss(animated flag: Bool)
/// 在一定时间之后执行自动隐藏
func performAutoDismiss(after delay: TimeInterval)
/// 取消自动隐藏
func cancelperformAutoDismiss()
/// 观察屏幕方向改变
func addOrientationDidChangeObserver(action: @escaping (CGSize) -> ()) -> NSObjectProtocol?
/// 取消屏幕方向观察
func removeOrientationDidChangeObserver(_ observer: NSObjectProtocol?)
/// 移除
func remove()
}
let color = UIColor.jRandom()
let texts = (1..<11).reversed().map { NSAttributedString(string: "\($0)", attributes: [.font: UIFont.systemFont(ofSize: 37), .foregroundColor: color]) }
view.jj.makeToast(JJVaryTextToastItem(attributedStrings: texts))
.updateItem(options: { options in
options.loopCount = 1
})
.duration(.distantFuture)
.autoDismissOnTap()
.show(animated: true)
使用makeToast
可以生成链式操作对象: JJToastDSL<T> where T: JJToastItemable
/// 根据对应的`toast`样式生成相应的链式操作
/// - Parameter item: `toast`
/// - Returns: `toast`链式操作
func makeToast<T>(_ item: T) -> JJToastDSL<T> where T: JJToastItemable {
JJToastDSL(view: base, item: item)
}
因为JJToastDSL
是泛型,所以可以调用updateItem
方法,配置对应toast
样式的配置
/// 修改`JJToastItemable`的配置
/// - Parameter block: block配置
func updateItem(options block: (_ options: inout T.Options) -> ()) -> Self {
block(&itemOptions)
return self
}
/// 使用渐变色容器
.useContainer(JJGradientContainer(colors: [.jRandom(), .jRandom(), .jRandom()]))
/// 显示动画
.appearAnimations([.scaleX(0.2), .opacity(0.3)])
/// 隐藏动画
.disappearAnimations([.scaleY(0.2).opposite, .opacity(0.3).opposite])
/// 点击自动消失
.autoDismissOnTap()
/// 时间:只要不主动隐藏就会永久显示
.duration(.distantFuture)
view.jj.show(message: text)
view.jj.showActivityIndicator()
@IBAction func showWebImage() {
guard let url = URL(string: "http://apng.onevcat.com/assets/elephant.png") else {
return
}
view.jj.makeToast(JJImageToastItem(url: url, display: { url, imageView in
imageView.sd_setImage(with: url, completed: nil)
})).updateItem(options: { opt in
opt.imageSize = .fixed(CGSize(width: 150, height: 150))
opt.configUIImageView = { iv in
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
}
})
.autoDismissOnTap()
.duration(.distantFuture)
.position(.center)
.show()
}
@IBAction func showUsingColorContainerTextToast() {
view.jj.makeToast(JJTextToastItem(text: "我是一个带色彩背景的toast"))
.useContainer(JJColorfulContainer(color: .jRandom()))
.duration(.distantFuture)
.autoDismissOnTap()
.show()
}
@IBAction func showMixActivityAndTextToast() {
view.jj.makeToast(JJMixTwoToastItem(first: JJActivityToastItem(), second: JJTextToastItem(text: "我是内容我是内容我是内容我是内容我是内容我是内容我是内容我是内容我是内容我是内容")))
.duration(.distantFuture)
.autoDismissOnTap()
.show()
}
你可以自定义任何你所需的样式, 然后makeToast
使用对应的对象,后续的配置+显示+隐藏逻辑,框架都以配置好。
present
类型modalPresentationStyle
为custom
,继承自UIPresentationController
以高斯模糊为蒙层,且居中弹出效果
let pd = JJAlertPresentationController(show: self, from: sourceController) { ctx in
ctx.usingBlurBelowCoverAnimators(style: .dark)
}
A.present(B, animated: true) {
let _ = pd
}
public var duration: TimeInterval = 0.2
public var belowCoverAction = JJAlertPresentationContext.BelowCoverAction.autodismiss(true)
行为action为枚举值
public enum BelowCoverAction {
/// 是否自动dismiss
case autodismiss(_ auto: Bool)
/// 自定义动作
case customize(action: () -> ())
}
可以在弹窗出现之后通过
AlertPresentationController
的updateContext
方法随时更改此属性 eg:可以在弹窗展示的时候为.autodismiss(false)
,然后,在页面事件处理完成之后改为.autodismiss(true)
同时,默认的点击空白消失是带动画的.如果不想带动画,请设置为.customize
,在block内部手动调用dismiss
public var frameOfPresentedViewInContainerView: ((_ containerViewBounds: CGRect, _ preferredContentSize: CGSize) -> (CGRect))? = Default.centerFrameOfPresentedView
public var presentationWrappingView: ((_ presentedViewControllerView: UIView, _ frameOfPresentedView: CGRect) -> UIView)? = Default.shadowAllRoundedCornerWrappingView(10)
public var belowCoverView: ((_ frame: CGRect) -> UIView)? = Default.dimmingBelowCoverView
public var transitionAnimator: ((_ fromView: UIView, _ toView: UIView, _ style: JJAlertPresentationContext.TransitionType, _ duration: TimeInterval, _ ctx: UIViewControllerContextTransitioning) -> ())? = Default.centerTransitionAnimator
public var willPresentAnimatorForBelowCoverView: ((_ belowCoverView: UIView, _ coordinator: UIViewControllerTransitionCoordinator) -> ())? = Default.dimmingBelowCoverViewAnimator(true)
public var willDismissAnimatorForBelowCoverView: ((_ belowCoverView: UIView, _ coordinator: UIViewControllerTransitionCoordinator) -> ())? = Default.dimmingBelowCoverViewAnimator(false)
A->B, B准守JJPushPopStylePresentDelegate
协议,然后在在跳转的时候设置B的transitioningDelegate
为自身
let b = UIViewController()
let navi = UINavigationController(rootViewController: b)
navi.modalPresentationStyle = .fullScreen
navi.transitioningDelegate = b.pushPopStylePresentDelegate
a.present(navi, animated: true)
- iOS 11.0+
- Swift 5.5+
Cocoapods
use_frameworks!
pod 'JJKit'