为您的iOS项目添加可定制的指南标记。适用于iPhone和iPad。
Instructions目前处于维护模式,因为我没有太多时间来维护这个项目。我会继续接受合并请求和修复问题,但短期内不会有新功能的期望。 |
目录
概述
特性
- 可定制高亮系统
- 可定制视图
- 可定制位置
- 跳过引导
- 可由代码控制
- 支持应用扩展
- 可动教练标记
- 从右到左支持
- 大小过渡支持(方向和分屏多任务)
- 支持部分
UIVisualEffectView
- 跨控制器引导
- 支持多个教练标记
需求
- Xcode 13 / Swift 5+
- iOS 14.0+
提问/贡献
提问
如果你需要帮助,请在Gitter房间提问。
贡献
如果你想贡献,请查看贡献指南。
安装
CocoaPods
将Instructions添加到Podfile中
source 'https://github.com/CocoaPods/Specs.git'
# Instructions is only supported for iOS 13+, but it
# can be used on older versions at your own risk,
# going as far back as iOS 9.
platform :ios, '9.0'
use_frameworks!
pod 'Instructions', '~> 2.2.0'
然后,运行以下命令
$ pod install
Carthage
将指令添加到您的Cartfile中
github "ephread/Instructions" ~> 2.2.0
然后可以更新、构建并将生成的框架拖入您的项目中
$ carthage update
$ carthage build
Swift Package Manager
在Xcode中,使用“文件”>“Swift Packages”>“添加包依赖”,并使用https://github.com/ephread/Instructions
。
手动安装
如果您不想使用CocoaPods和Carthage,可以手动安装Instructions,但需要自行管理更新。
内嵌框架
- 将Instructions.xcodeproj拖至您的应用程序的Xcode项目导航器中。
- 仍处于项目导航器中,选择您的应用程序项目。应该会出现目标配置面板。
- 选择适当的目标,然后在“通用”面板中向下滚动到“内嵌二进制”部分。
- 点击加号按钮,然后在“产品”目录下选择“Instructions.framework”。
使用
入门
打开您希望显示引导标记的控制器,并实例化一个新的CoachMarksController
。您还应该提供一个dataSource
,一个遵循CoachMarksControllerDataSource
协议的对象。
class DefaultViewController: UIViewController,
CoachMarksControllerDataSource,
CoachMarksControllerDelegate {
let coachMarksController = CoachMarksController()
override func viewDidLoad() {
super.viewDidLoad()
self.coachMarksController.dataSource = self
}
}
数据源
CoachMarksControllerDataSource
声明了三个强制方法。
第一个方法要求显示的教练标记数量。假设您只想显示一个教练标记。请注意,请求信息的CoachMarksController
被提供,允许您在单个数据源内为多个CoachMarksController
提供数据。
func numberOfCoachMarks(for coachMarksController: CoachMarksController) -> Int {
return 1
}
第二个方法要求元数据。这允许您自定义教练标记的位置和外观,但不会让您定义其外观(稍后会有更多介绍)。元数据被封装在一个名为CoachMark
的结构中。注意参数coachMarkAt
,它可以为您提供教练标记的语义位置,就像IndexPath
所做的那样。coachMarksController
提供了一种简单的方式来创建来自给定视图的默认CoachMark
对象。
let pointOfInterest = UIView()
func coachMarksController(_ coachMarksController: CoachMarksController,
coachMarkAt index: Int) -> CoachMark {
return coachMarksController.helper.makeCoachMark(for: pointOfInterest)
}
第三个方法以元组的形式提供两个视图(类似于cellForRowAtIndexPath
)。body视图是必需的,因为它是教练标记的核心。箭头视图是可选的。
但现在,我们将仅返回Instructions提供的默认视图。
func coachMarksController(
_ coachMarksController: CoachMarksController,
coachMarkViewsAt index: Int,
madeFrom coachMark: CoachMark
) -> (bodyView: UIView & CoachMarkBodyView, arrowView: (UIView & CoachMarkArrowView)?) {
let coachViews = coachMarksController.helper.makeDefaultCoachViews(
withArrow: true,
arrowOrientation: coachMark.arrowOrientation
)
coachViews.bodyView.hintLabel.text = "Hello! I'm a Coach Mark!"
coachViews.bodyView.nextLabel.text = "Ok!"
return (bodyView: coachViews.bodyView, arrowView: coachViews.arrowView)
}
开始教练标记流程
一旦设置了,您就可以开始显示教练标记。您很可能会将
self
提供给start
。虽然蒙版将自己作为当前窗口的子项添加(位于所有内容的上方),但CoachMarksController
将自己作为您提供的视图控制器的子项添加。当收到大小变化事件时,CoachMarksController
将相应地做出反应。请注意:您不能在viewDidLoad
方法中调用start
,因为视图层次结构必须设置好并准备好Instructions才能正确工作。
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.coachMarksController.start(in: .window(over: self))
}
停止教练标记流程
视图消失时,您应该始终停止流程。为了避免动画假象和时序问题,不要忘记将以下代码添加到您的viewWillDisappear
方法中。调用stop(immediately: true)
将确保视图消失时立即停止流程。
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.coachMarksController.stop(immediately: true)
}
您已设置完毕。您可以在库提供的Examples/
目录中查看更多示例。
高级使用
自定义覆盖层
您可以使用此属性自定义覆盖层的背景颜色
overlay.backgroundColor
您还可以使覆盖层模糊后面的内容。将此属性设置为非nil
将禁用overlay.backgroundColor
overlay.blurEffectStyle: UIBlurEffectStyle?
您可以使覆盖层可点击。点击覆盖层将隐藏当前教练标记并显示下一个
overlay.isUserInteractionEnabled: Bool
您还可以允许在剪贴路径内发生的触摸事件向下面的UIView转发...
overlay.isUserInteractionEnabledInsideCutoutPath: Bool
……或者您可以让整个覆盖层将触摸事件转发给下面的视图。
overlay.areTouchEventsForwarded: Bool
警告阴影覆盖层在应用扩展中不受支持。
自定义默认教练标记
默认教练标记提供最少的自定义选项。
在CoachMarkBodyDefaultView
和CoachMarkArrowDefaultView
中均可使用
background.innerColor: UIColor
:教练标记的背景颜色。background.borderColor: UIColor
:教练标记的边框颜色。background.highlightedInnerColor: UIColor
:教练标记高亮时的背景颜色。background.highlightedBorderColor: UIColor
:教练标记高亮时的边框颜色。
仅应用于CoachMarkArrowDefaultView
background.cornerRadius: UIColor
:教练标记的圆角半径。
您还可以自定义CoachMarkBodyDefaultView.hintLabel
和CoachMarkBodyDefaultView.nextLabel
的属性。例如,您可以在教练标记中更改nextLabel
的位置
let coachViews = coachMarksController.helper.makeDefaultCoachViews(
withArrow: true,
arrowOrientation: coachMark.arrowOrientation
nextLabelPosition: .topTrailing
)
coachViews.bodyView.hintLabel.text = "Hello! I'm a Coach Mark!"
coachViews.bodyView.nextLabel.text = "Ok!"
参考MixedCoachMarksViewsViewController.swift
和NextPositionViewController.swift
以获取实际示例。
提供自定义视图
如果默认的自定义选项不够,您可以提供您自己的自定义视图。标签指示器由一个 主体 视图和一个 箭头 视图组成。请注意,术语 箭头 可能有些误导。它不必是一个真实的箭头;它可以是你想要的任何东西。
主体视图必须符合 CoachMarkBodyView
协议。箭头视图必须符合 CoachMarkArrowView
协议。它们也必须是 UIView
的子类。
返回 CoachMarkBodyView
视图是强制性的,而返回 CoachMarkArrowView
是可选的。
CoachMarkBodyView 协议
此协议定义了两个属性。
-
nextControl: UIControl? { get }
你必须在你的视图中实现这个属性的getter方法;这将使CoachMarkController
知道应该点击哪个控件以显示下一个标签指示器。请注意,这不必是一个子视图;你还可以返回视图本身。 -
highlightArrowDelegate: CoachMarkBodyHighlightArrowDelegate?
如果视图本身是接收点击的控件,你可能想将其高亮状态转发给箭头视图(使它们看起来像同一个组件)。CoachMarkController
将自动设置适当的代理到这个属性。然后你可以这样做
override var highlighted: Bool {
didSet {
self.highlightArrowDelegate?.highlightArrow(self.highlighted)
}
}
考虑方向
记得从dataSource获取以下方法?
func coachMarksController(
_ coachMarksController: CoachMarksController,
coachMarkViewsAt index: Int,
madeFrom coachMark: CoachMark
) -> (bodyView: UIView & CoachMarkBodyView, arrowView: (UIView & CoachMarkArrowView)?) {
let coachViews = coachMarksController.helper.makeDefaultCoachViews(
withArrow: true,
arrowOrientation: coachMark.arrowOrientation
)
}
当提供自定义视图时,您需要为箭头视图提供适当的朝向(例如,当是实际的箭头时,向上或向下)。CoachMarkController
将通过以下属性告诉您预期的方向:CoachMark.arrowOrientation
。
查看Example]}
目录以获取更多信息。
提供自定义截断路径
如果你不喜欢默认截断路径的显示效果,你可以通过为 makeCoachMark(for:)
提供一个块来自定义它。截断路径将自动存储在返回的 CoachMark
对象的 cutoutPath
属性中。
var coachMark = coachMarksController.helper.makeCoachMark(
for: customView,
cutoutPathMaker: { (frame: CGRect) -> UIBezierPath in
// This will create an oval cutout a bit larger than the view.
return UIBezierPath(ovalIn: frame.insetBy(dx: -4, dy: -4))
}
)
frame
是 customView
在 coachMarksController.view
坐标空间中的框架。此坐标空间与指令坐标空间的转换是自动处理的。你可以提供任何形状,从简单的矩形到复杂的五角星。
如果你提供了其坐标空间,你也可以直接传递一个框架矩形。
var coachMark = coachMarksController.helper.makeCoachMark(
forFrame: frame,
in: superview,
cutoutPathMaker: { (frame: CGRect) -> UIBezierPath in
// This will create an oval cutout a bit larger than the view.
return UIBezierPath(ovalIn: frame.insetBy(dx: -4, dy: -4))
}
)
呈现上下文
你可以通过将其传递给 `start(in:) 来选择教练标记将显示的上下文。可用的上下文包括
.newWindow(over: UIViewController, at: UIWindowLevel?)
– 在给定的UIWindowLevel
下创建一个新窗口(在应用扩展中不可用);.currentWindow(of: UIViewController)
– 显示给定UIViewController
的窗口;.viewController(_: UIViewController)
– 在给定UIViewController
的view
中。
此外,你还可以使用 window(over: UIViewController)
,这是一个等于调用 .newWindow(over: UIViewController, at: UIWindowLevelNormal + 1)
的便利静态方法。
警告 将窗口级别设置为
UIWindowLevelStatusBar
以上的任何级别在 iOS 13+ 或在添加模糊效果到覆盖物上时不受支持。
当教练标记在 .newWindow
上下文中显示时,自定义窗口通过 CoachMarkController
的 rootWindow
属性公开。
自定义教练标记显示方式
您可以自定义以下属性
-
gapBetweenBodyAndArrow: CGFloat
:给定教练标记中 body 和 arrow 之间的垂直间隔。 -
pointOfInterest: CGPoint?
:箭头将面对的点。目前,它仅用于水平移位箭头,使其位于焦点之上或之下。 -
gapBetweenCoachMarkAndCutoutPath: CGFloat
:教练标记和截断路径之间的间隔。 -
maxWidth: CGFloat
:教练标记可以接受的最大宽度。您不希望教练标记太宽,尤其是在iPad上。 -
horizontalMargin: CGFloat
是叠加视图边缘与教练标记之间的间距(包括首尾)。请注意,如果教练标记的最大宽度小于叠加视图的宽度,您的视图将左侧或右侧堆叠,另一侧会留有空间。 -
arrowOrientation: CoachMarkArrowOrientation?
是箭头的方向(不是教练标记的方向,意味着将此属性设置为.Top
将使教练标记显示在兴趣点下方)。尽管这通常由库预先计算,但您可以在coachMarksForIndex:
或coachMarkWillShow:
中覆盖它。 -
isDisplayedOverCutoutPath: Bool
启用教练标记在剪切路径上显示;请注意,如果将此属性设置为true
,则箭头将不可见。 -
isOverlayInteractionEnabled: Bool
用于根据情况禁用点击叠加来显示下一个教练标记的能力;默认为true
。 -
isUserInteractionEnabledInsideCutoutPath: Bool
用于允许在剪切路径内进行触摸传递。有关更多信息,请参阅Example/
目录中的TransitionFromCodeViewController
。
动画教练标记
要动画化教练标记,您需要实现CoachMarksControllerAnimationDelegate
协议。
func coachMarksController(
_ coachMarksController: CoachMarksController,
fetchAppearanceTransitionOfCoachMark coachMarkView: UIView,
at index: Int,
using manager: CoachMarkTransitionManager
)
func coachMarksController(
_ coachMarksController: CoachMarksController,
fetchDisappearanceTransitionOfCoachMark coachMarkView: UIView,
at index: Int,
using manager: CoachMarkTransitionManager
)
func coachMarksController(
_ coachMarksController: CoachMarksController,
fetchIdleAnimationOfCoachMark coachMarkView: UIView,
at index: Int,
using manager: CoachMarkAnimationManager
)
该代理的所有方法都以类似的方式工作。首先,您需要通过manager.parameters
属性指定动画的一般参数。这些属性与您可以向UIView.animate
提供的配置参数匹配。
-
duration: TimeInterval
:动画的总持续时间。 -
delay: TimeInterval
:开始动画前要等待的时间。 -
options: UIViewAnimationOptions
:一个选项掩码,指示您想要如何执行动画(对于常规动画)。 -
keyframeOptions: UIViewKeyframeAnimationOptions
:一个选项掩码,指示您想要如何执行动画(对于关键帧动画)。
设置参数后,您应通过调用manager.animate
提供您的动画。该方法签名取决于您是动画化教练标记的空闲状态还是显示/隐藏它们。
您应通过传递给animate
参数的块提供您的动画,类似于UIView.animate
。如果您需要访问动画参数或教练标记元数据,将提供一个包含这些内容的CoachMarkAnimationManagementContext
,并将其提供给您的动画块。您不应从动画块中捕获对管理器的引用。
对于实现示例,您还可以查看Example
目录中的DelegateViewController
类。
出现和消失的详细信息
如果您需要定义初始状态,应将一个块提供给 fromInitialState
属性。虽然在前一个方法中直接在 coachMarkView
上设置值,然后再调用 manager.animate()
可能会生效,但并不保证。
让用户跳过教程
控制
您可以为用户提供一种跳过教练标记的方法。首先,您需要使用一个遵循 CoachMarkSkipView
协议的 UIView
来设置 skipView
。此协议定义了一个属性
public protocol CoachMarkSkipView: AnyObject {
var skipControl: UIControl? { get }
}
您必须在您的视图中实现此属性的获取方法。这将让 CoachMarkController
知道应该点击哪个控件来跳过教程。同样,它不必是子视图;您可以返回视图本身。
通常,Instructions 为 CoachMarkSkipView
提供了一个名为 CoachMarkSkipDefaultView
的默认实现。
数据源
要定义视图如何定位自身,您可以使用来自 CoachMarkControllerDataSource
协议的方法。此方法为可选。
func coachMarksController(
_ coachMarksController: CoachMarksController,
constraintsForSkipView skipView: UIView,
inParent parentView: UIView
) -> [NSLayoutConstraint]?
此方法将由 CoachMarksController
在开始教程之前以及每次尺寸变化时调用。它提供了将放置的位置的视图和返回数组 NSLayoutConstraints
。这些约束将定义 跳过按钮 将如何放置在父视图中。您不应该添加约束;只需返回它们。
返回 nil
会告诉 CoachMarksController
使用默认约束,这将在屏幕顶部放置 跳过按钮。建议不要返回空数组,因为它可能会导致尴尬的定位。
有关跳转机制的更多信息,请参阅 Example/
目录。
从代码中导航流程
如果需要通过编程方式显示教练标记,CoachMarkController.flow
还提供了以下方法
func showNext(numberOfCoachMarksToSkip numberToSkip: Int = 0)
func showPrevious(numberOfCoachMarksToSkip numberToSkip: Int = 0)
您可以指定要跳过的教练标记数量(跳转到不同的索引)。
请查看 TransitionFromCodeViewController
在 Example/
目录下的代码,了解如何利用该方法让用户执行特定的操作。
使用代理
CoachMarkController
会在多个时刻通知代理。所有这些方法都是可选的。
首先,在教练标记显示时。你可能想更改视图中的某些内容。因此,CoachMark
元数据结构被传递为一个 inout
对象,以便你可以使用新参数更新它。
func coachMarksController(
_ coachMarksController: CoachMarksController,
willShow coachMark: inout CoachMark,
at index: Int
)
其次,当教练标记消失时。
func coachMarksController(
_ coachMarksController: CoachMarksController,
willHide coachMark: CoachMark,
at index: Int
)
第三,当所有教练标记都已显示。didEndShowingBySkipping
指定流程是否因为用户请求结束而完成。
func coachMarksController(
_ coachMarksController: CoachMarksController,
didEndShowingBySkipping skipped: Bool
)
当用户点击遮罩层时的响应
用户点击遮罩层时,你将通过以下方式得到通知
func shouldHandleOverlayTap(
in coachMarksController: CoachMarksController,
at index: Int
) -> Bool
返回 true
将允许说明继续流程,而返回 false
将中断它。如果你选择中断流程,你将负责停止或暂停它或手动显示下一个教练标记(见 从代码中引导流程)。
index
是当前显示的教练标记的索引。
暂停和恢复流程
只需调用 coachMarksController.flow.pause()
和 coachMarksController.flow.resume()
就这么简单。暂停时,你也可以选择完全隐藏 Instructions 的遮罩层(.pause(and: hideInstructions)
),或者只隐藏遮罩层并保留其触摸阻止能力(.pause(and: hideOverlay)
)。
在显示教练标记之前执行动画
您可以在显示教练标记之前或之后对视图进行动画处理。例如,您可能想在提及这些表头之前,先折叠表格视图并只显示其标题。指令提供了一个简单的方法将动画插入到流程中。
例如,假设您想在教练标记出现之前执行一个动画。您需要将一些逻辑放入 coachMarkWillShow
委托方法中。为了确保您不必进行修改,将异步动画块转换为同步的,您可以暂停流程,执行动画,然后再开始流程。这将确保您的UI永远不会停滞。
func coachMarksController(
_ coachMarksController: CoachMarksController,
willShow coachMark: inout CoachMark,
at index: Int
) {
// Pause to be able to play the animation and then show the coach mark.
coachMarksController.flow.pause()
// Run the animation
UIView.animateWithDuration(1, animations: { () -> Void in
…
}, completion: { (finished: Bool) -> Void in
// Once the animation is completed, we update the coach mark,
// and start the display again. Since inout parameters cannot be
// captured by the closure, you can use the following method to update
// the coach mark. It will only work if you paused the flow.
coachMarksController.helper.updateCurrentCoachMark(using: myView)
coachMarksController.flow.resume()
})
}
如果您需要更新教练标记的多个属性,您可能更喜欢基于块的策略。当更新兴趣点和裁剪路径时,请使用提供的转换器在指令的坐标空间中表达它们。
coachMarksController.helper.updateCurrentCoachMark { coachMark, converter in
coachMark.pointOfInterest = converter.convert(point: myPoint, from: myPointSuperview)
coachMark.gapBetweenCoachMarkAndCutoutPath = 6
}
警告 由于模糊覆盖层在教练标记出现/消失时捕获视图快照,您应确保在教练标记出现或消失时不要对您的视图执行目标动画。否则,动画将不可见。
您还可以自定义经典的不透明度覆盖层,因为当 UIAccessibility.isReduceTransparencyEnabled
返回 true 时,指令将回退到使用传统类型。
跳过教练标记
您可以通过实现定义在 CoachMarksControllerDelegate
中的以下方法来跳过一个特定的教练标记
func coachMarksController(
_ coachMarksController: CoachMarksController,
coachMarkWillLoadAt index: Int
) -> Bool
coachMarkWillLoadAt:
在显示特定教练标记之前被调用。要防止显示 CoachMark,您可以从此方法返回 false
。
自定义覆盖层的装饰
通过实现以下 CoachMarksControllerDelegate
中的方法,您可以添加自定义视图,这些视图将显示在覆盖层之上
func coachMarksController(
_ coachMarksController: CoachMarksController,
configureOrnamentsOfOverlay overlay: UIView
)
只需将装饰添加到提供视图(overlay
)中,并且指令应该会处理好剩下的。请注意,但是,这些装饰将显示在裁剪之下但教练标记之上。
处理框架变化
由于指令中没有引用感兴趣的观点,因此不能自动响应用户界面的变化。
指令提供了两种处理框架变化的方法。
CoachMarkController.prepareForChange()
,在框架变化之前调用,用于隐藏教练标记和裁剪路径。CoachMarkController.restoreAfterChangeDidComplete()
,在框架变化后调用,用于再次显示教练标记和裁剪路径。
虽然您可以在 Instructions 空闲时随时调用这些方法,但如果教练标记已经显示,结果将不会平滑。更好的做法是通过暂停和恢复流程在两个教练标记之间执行更改。KeyboardViewController
展示了这种技术的示例。
在应用扩展内部的使用
如果您想在天体扩展中添加 Instructions,您需要执行一些额外的操作。示例可以在App Extensions Example/
目录中找到。
依赖关系
Instructions包含两个共享计划,分别是Instructions
和InstructionsAppExtensions
。这两个计划的唯一区别在于,InstructionsAppExtensions
不依赖于UIApplication.sharedApplication()
,适用于应用扩展。
在下面的示例中,让我们考虑一个有两个目标的工程,一个是普通应用(Instructions App Extensions Example
),另一个是应用扩展(键盘扩展
)。
CocoaPods
如果您使用 CocoaPods 导入 Instructions,需要编辑您的 Podfile
使其看起来是这样的
target 'Instructions App Extensions Example' do
pod 'Instructions', '~> 2.2.0'
end
target 'Keyboard Extension' do
pod 'InstructionsAppExtensions', '~> 2.2.0'
end
如果 Instructions 只是从应用扩展目标中导入的,您不需要第一个块。
编译任何一个目标时,CocoaPods 都会确保设置正确的标志,从而允许/禁止对 UIApplication.sharedApplication()
的调用。您不需要更改您的代码。
框架(Carthage / 手动管理)
如果您通过框架导入指令,会发现有两种共享方案。(Instructions
和 InstructionsAppExtensions
)它们都会导致不同的框架。
您需要嵌入这两个框架并将它们链接到正确的目标上。确保它们看起来像这样
Instructions App Extensions 示例
如果您计划只将指令添加到 App 扩展目标,则不需要添加 Instructions.frameworks
。
导入语句
当从 Instructions App Extensions 示例
中的文件导入指令时,您应该使用常规导入语句
import Instructions
但是,当从 键盘扩展
中的文件导入指令时,您应该使用特定语句
import InstructionsAppExtensions
警告在应用扩展中导入 Instructions 是可能的。但是,您有很大的风险被 Apple Store 拒绝。使用
UIApplication.sharedApplication()
的应用是在编译时进行了静态检查,但在运行时没有阻止您进行调用。幸运的是,Xcode 应该会警告您是否错误地链接了不适用于应用扩展的框架。
许可证
Instructions 是在 MIT 许可证下发布的。详细资料请见 LICENSE 文件。