PolarisGuideKit
一个轻量的基于 UIKit 的新手引导(coach marks)组件:通过遮罩挖孔高亮 UI 元素,并支持搭配说明视图(Buddy View)与按步骤编排的引导流程。
功能特性
- 单个 focusView 高亮(遮罩挖孔 + 可选过渡动画):
GuideOverlayView - 高亮区域自动跟踪 focusView 位置变化,适配屏幕旋转、动画等场景
- 以步骤编排的新手引导流程:
GuideController+GuideStep - 通过继承实现任意说明/箭头/按钮视图:
GuideBuddyView - 可自定义的自动"完成条件":
GuideAutoCompleter、ControlEventCompleter、GestureRecognizerCompleter - 可插拔的步骤扩展(插件 + 附件,例如音频播放、埋点事件上报、引导显示标志保存等)
- 侵入性小,能很好地保持原有业务逻辑
环境要求
- iOS 12+
- Swift 5.0+
- UIKit
安装
Swift Package Manager
- Xcode:File → Add Packages…
- 填入仓库地址:
https://github.com/noodles1024/PolarisGuideKit - 选择 PolarisGuideKit
CocoaPods
在 Podfile 中添加:
pod 'PolarisGuideKit'
然后在代码里:
import PolarisGuideKit快速开始
import UIKit import PolarisGuideKit final class MyViewController: UIViewController { private var guide: GuideController? override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let step1 = GuideStep() step1.focusView = myButton step1.buddyView = MyBuddyView() step1.forwardsTouchEventsToFocusView = true step1.completer = ControlEventCompleter(control: myButton, event: .touchUpInside) let controller = GuideController(hostView: view, steps: [step1]) controller.onDismiss = { _, context in print("引导结束。原因=\(context.reason)") } _ = controller.show() guide = controller } }
自定义 Buddy View(说明/箭头/按钮等)
继承 GuideBuddyView,搭建 UI,在合适的时机调用 requestNext() / requestSkip()。
import UIKit import PolarisGuideKit final class MyBuddyView: GuideBuddyView { private let nextButton = UIButton(type: .system) override init(frame: CGRect) { super.init(frame: frame) nextButton.setTitle("下一步", for: .normal) nextButton.addTarget(self, action: #selector(onNext), for: .touchUpInside) addSubview(nextButton) // 自行布局... } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func updateLayout(referenceLayoutGuide layoutGuide: UILayoutGuide, focusView: UIView) { super.updateLayout(referenceLayoutGuide: layoutGuide, focusView: focusView) // 根据 layoutGuide 相对定位你的子视图(避免遮挡高亮区域) } @objc private func onNext() { requestNext() } }
自定义 FocusStyle(高亮形状)
实现 FocusStyle,控制挖孔形状(例如圆角等)。
import UIKit import PolarisGuideKit struct MyRoundedStyle: FocusStyle { func highlightPath(for focusView: UIView, frameInOverlay rect: CGRect) -> UIBezierPath? { UIBezierPath(roundedRect: rect.insetBy(dx: -8, dy: -8), cornerRadius: 12) } func buddyLayoutGuide(for focusView: UIView, shadowView: GuideShadowView) -> UILayoutGuide { shadowView.frameLayoutGuide } }
动态 FocusView(UITableView/UICollectionView)
当高亮 UITableView 或 UICollectionView 中的 cell 时,reloadData 后 cell 可能会被复用,导致高亮区域错位或消失。使用 focusViewProvider 动态获取正确的 cell:
let step = GuideStep() // 使用 focusViewProvider 动态查找 cell step.focusViewProvider = { [weak self] in guard let self else { return nil } var focusCell = self.tableView.cellForRow(at: targetIndexPath) if focusCell == nil { self.tableView.layoutIfNeeded() focusCell = self.tableView.cellForRow(at: targetIndexPath) } return focusCell } step.buddyView = MyBuddyView()
每次需要更新高亮时都会调用此闭包,确保始终高亮正确的 cell,即使 reloadData 后也不受影响。
提示:对于不会改变的静态视图,仍可直接使用
step.focusView = myView。
插件与附件
PolarisGuideKit 支持通过插件系统扩展功能。插件可以在引导流程的不同阶段执行自定义逻辑,附件则用于存储每一步的数据。
内置插件示例
音频播放插件:为步骤添加背景音乐或语音解说
let step = GuideStep() step.focusView = myButton // 添加音频附件 if let url = Bundle.main.url(forResource: "guide_step_1", withExtension: "mp3") { step.addAttachment(GuideAudioAttachment(url: url, volume: 0.8)) } // 启用音频插件 let controller = GuideController(hostView: view, steps: [step], plugins: [AudioGuidePlugin()])
需要接收插件事件的 Buddy View 可以按需实现协议:
final class MyBuddyView: GuideBuddyView, GuideAudioEventReceiving { func guideAudioDidStart() { /* update UI */ } func guideAudioDidStop(didPlayToEnd: Bool) { /* update UI */ } func guideAudioDidFail(_ error: Error) { /* update UI */ } }
自定义插件
你可以通过实现 GuidePlugin 协议来创建自己的插件:
final class MyCustomPlugin: GuidePlugin { func guideController(_ controller: GuideController, willShowGuideWith context: GuideContext) { // 引导开始时的逻辑 } func guideController(_ controller: GuideController, didCompleteStep step: GuideStep, at index: Int, with context: GuideContext) { // 步骤完成时的逻辑 } func guideController(_ controller: GuideController, didDismissGuideWith context: GuideContext) { // 引导结束时的逻辑 } } // 使用自定义插件 let controller = GuideController(hostView: view, steps: steps, plugins: [MyCustomPlugin()])
stepDidShow 在步骤配置完成后触发,guideDidShow 在(如果开启动画)遮罩淡入完成后触发;依赖“界面完全可见”的插件应等待 guideDidShow。
架构
flowchart TB
subgraph Core["核心组件"]
GuideController["GuideController<br/>(流程编排器)"]
GuideStep["GuideStep<br/>(步骤配置)"]
end
subgraph ViewHierarchy["视图层级"]
GuideContainerView["GuideContainerView<br/>(透明容器)"]
GuideOverlayView["GuideOverlayView<br/>(遮罩 + 触摸转发)"]
MaskOverlayView["MaskOverlayView<br/>(遮罩基类)"]
GuideBuddyView["GuideBuddyView<br/>(说明视图)"]
GuideShadowView["GuideShadowView<br/>(焦点追踪器)"]
end
subgraph Extensions["扩展机制"]
FocusStyle["FocusStyle<br/>(高亮形状)"]
GuideAutoCompleter["GuideAutoCompleter<br/>(完成触发器)"]
GuidePlugin["GuidePlugin<br/>(生命周期钩子)"]
GuideStepAttachment["GuideStepAttachment<br/>(插件数据)"]
end
GuideController -->|"管理"| GuideStep
GuideController -->|"创建并承载"| GuideContainerView
GuideController -->|"派发事件"| GuidePlugin
GuideContainerView -->|"包含"| GuideOverlayView
GuideContainerView -->|"包含"| GuideBuddyView
GuideOverlayView -.->|"继承"| MaskOverlayView
GuideOverlayView -->|"创建"| GuideShadowView
GuideOverlayView -->|"使用"| FocusStyle
GuideStep -->|"配置"| GuideBuddyView
GuideStep -->|"使用"| FocusStyle
GuideStep -->|"通过...触发"| GuideAutoCompleter
GuideStep -->|"携带"| GuideStepAttachment
常见问题
点击事件没有透传到 focusView
- 确保
step.forwardsTouchEventsToFocusView = true - 确保 focusView 与 overlay 在同一个 window
手势识别器没有触发
- 确保手势已加入
step.gestureRecognizerWhitelist - 如果手势挂在 tableView/collectionView 上,仍需要白名单放行
多 window / 多 scene 场景
建议显式传入 hostView(GuideController(hostView:steps:)),避免"key window"歧义。
License
本仓库采用 MIT License,详见 LICENSE。
支持开发者
如果你觉得这个项目对你有帮助,可以考虑支持我的开源工作 ☕
微信赞赏支持
或者通过以下方式支持:
- ⭐ 在 GitHub 上给项目点星
- 🐛 提交 Issue 报告问题或建议功能
- 🔀 提交 Pull Request 贡献代码
- 📢 在社交媒体上分享这个项目

