说明 2.3.0

说明 2.3.0

测试已测试
语言语言 SwiftSwift
许可 MIT
发布最新版本Sep 2023
SPM支持SPM

Frédéric Maquin维护。



说明 2.3.0

  • 作者:
  • Frédéric Maquin

Instructions

Build status Maintainability Coverage CocoaPods Shield Carthage compatible Join the chat at https://gitter.im/ephread/Instructions

为您的iOS项目添加可定制的教练标记。适用于iPhone和iPad。

⚠️维护者信息
Instructions目前处于维护模式,因为我没有太多时间来处理这个项目。我会继续接受合并请求和修复问题,但短期内不应期望新功能。

目录

概览

Instructions Demo

特性

要求

  • 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,但需要自己管理更新。

嵌入式框架

  1. 将“Instructions.xcodeproj”拖到您应用的Xcode项目的“项目导航器”中。
  2. 仍在项目导航器中,选择您的应用程序项目。应该会显示目标配置面板。
  3. 选择适当的目标,在“通用”面板中,滚动到“嵌入式二进制文件”部分。
  4. 单击+按钮,并在“产品”目录下选择“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)
}

第三个是提供两种视图的形式,即元组。必须提供body视图,因为它是说明标记的核心。可选的是arrow视图。

但在此期间,我们只返回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)
}

开始说明标记流程

设置数据源后,您可以开始显示说明标记。您很可能会向start提供self。虽然覆盖层将自己添加为当前窗口的子项(以保持在所有内容之上),但CoachMarksController将将自己添加为您提供的视图控制器。当CoachMarksController接收到大小变化事件时,它将相应地做出反应。请注意;由于视图层级必须设置并准备好供Instructions正确工作,因此您不能在viewDidLoad方法中调用start

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

警告 在应用程序扩展中不支持模糊叠加层。

自定义默认教练标记

默认教练标记提供最小的自定义选项。

CoachMarkBodyDefaultViewCoachMarkArrowDefaultView 中都可使用

  • background.innerColor: UIColor:教练标记的背景颜色。
  • background.borderColor: UIColor:教练标记的边框颜色。
  • background.highlightedInnerColor: UIColor:当教练标记被高亮时,教练标记的背景颜色。
  • background.highlightedBorderColor: UIColor:当教练标记被高亮时,教练标记的边框颜色。

仅适用于 CoachMarkArrowDefaultView

  • background.cornerRadius: UIColor:教练标记的圆角半径。

您还可以自定义 CoachMarkBodyDefaultView.hintLabelCoachMarkBodyDefaultView.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.swiftNextPositionViewController.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 是以 coachMarksController.view 的坐标空间表示的 customView 的框架。这个坐标空间和说明坐标空间之间的转换被自动处理。可以提供任何形状,从简单的矩形到复杂的星形。

如果提供坐标空间,也可以直接传递框架矩形。

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: PresentationContext)` 来选择教练标记将要显示的上下文,可用的上下文包括

  • .newWindow(over: UIViewController, at: UIWindowLevel?) - 在给定的 UIWindowLevel 创建一个新窗口(在应用扩展中不可用);
  • .currentWindow(of: UIViewController) - 显示给定的 UIViewController 的窗口;
  • .viewController(_: UIViewController) - 在给定的 UIViewControllerview 中。

此外,您还可以使用 window(over: UIViewController),这是一个等价于调用 .newWindow(over: UIViewController, at: UIWindowLevelNormal + 1) 的便利静态方法。

警告 将窗口级别设置为 UIWindowLevelStatusBar 以上的级别在 iOS 13+ 或在叠加层添加模糊效果时不支持。

当教练标记在 . newWindow 上下文中显示时,可以通过 CoachMarkControllerrootWindow 属性获取自定义窗口。

自定义教练标记显示方式

您可以自定义以下属性

  • gapBetweenBodyAndArrow: CGFloat:给定教练标记中 bodyarrow 之间的垂直间隔。

  • 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 属性提供块。虽然在不调用 manager.animate() 方法之前直接在 coachMarkView 上设置值可能有效,但这不能保证。

允许用户跳过巡视

控制

您可以为用户提供一种跳过引导标记的方法。首先,您需要使用符合CoachMarkSkipView协议的UIView设置skipView。此协议定义单个属性。

public protocol CoachMarkSkipView: AnyObject {
    var skipControl: UIControl? { get }
}

您必须在您的视图中实现此属性的getter方法。这将让CoachMarkController知道哪个控件应被点击以跳过巡视。再次强调,它不必是子视图;您可以返回视图本身。

像往常一样,Instructions提供了名为CoachMarkSkipDefaultView的默认CoachMarkSkipView实现。

数据源

要定义视图将如何定位自身,您可以使用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)

您可以指定要跳过的引导标记的数量(向前或向后跳转到不同的索引)。

请参阅Example/目录中的TransitionFromCodeViewController,了解如何利用此方法让用户执行特定操作。

使用代理

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))。

在显示教练标记之前执行动画

你可以在显示教练标记之前或之后对视图执行动画。例如,你可能想要在参考那些标题之前折叠表格视图并只显示其标题。Instructions提供了一种简单的方法将你的动画插入到流程中。

例如,假设您希望在教练标记显示之前执行动画。您将在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:是在一个教练标记即将显示之前被调用的。为了防止教练标记显示,您可以从该方法中返回false

自定义叠加装饰

您可以通过实现以下方法来为叠加添加自定义视图,这是在实现CoachMarksControllerDelegate中。

func coachMarksController(
    _ coachMarksController: CoachMarksController,
    configureOrnamentsOfOverlay overlay: UIView
)

只需将装饰添加到提供的视图(overlay)中,Instructions应该会处理其余的事情。请注意,但无论如何,这些装饰将在剪裁之下但在教练标记之上显示。

处理帧变化

由于Instructions不保留对兴趣视图的引用,它无法自动响应它们的帧变化。

说明提供了两种处理框架更改的方法。

  • 在框架更改之前调用 CoachMarkController.prepareForChange(),以隐藏教练标记和裁剪路径。
  • 在框架更改后调用 CoachMarkController.restoreAfterChangeDidComplete(),再次显示教练标记和裁剪。

尽管您可以在 Instructions 闲置时随时调用这些方法,但如果教练标记已被显示,结果将不会看起来很平滑。最好在两个教练标记之间通过暂停和恢复流程来执行更改。有关此技术的示例,请参阅 KeyboardViewController

在应用程序扩展中使用

如果您想在应用程序扩展中添加 Instructions,还需要执行一些额外的工作。该示例可在 App Extensions Example/ 目录中找到。

依赖项

Instructions 拥有两个共享方案,分别是 InstructionsInstructionsAppExtensions。这两个方案之间的唯一区别是 InstructionsAppExtensions 不依赖于 UIApplication.sharedApplication(),这使得它适合用于应用程序扩展。

以下示例中,让我们考虑一个有两个目标的项目,一个是常规应用程序(Instructions App Extensions Example),另一个是应用程序扩展(Keyboard Extension)。

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 / 手动管理)

如果您通过框架导入指令,您会注意到两种共享方案。《指令》和《指令AppExtensions》都生成了不同的框架。

您需要嵌入这两个框架并将它们链接到正确的目标。确保它们看起来像这样

指令 App Extensions 示例 Imgur

键盘扩展 Imgur

如果您只计划将指令添加到 App Extensions 目标,您不需要添加《指令.frameworks》。

导入语句

当从《指令 App Extensions 示例》中的文件导入指令时,您应使用常规的导入语句

import Instructions

但是,当从《键盘扩展》中的文件导入指令时,您应使用特殊的语句

import InstructionsAppExtensions

警告 在应用扩展中导入指令是可能的。然而,您被苹果商店拒绝的风险很高。在编译过程中会静态检查对UIApplication.sharedApplication()的使用,但在运行时没有阻止您进行调用。幸运的是,如果您无意中链接了一个不适合应用扩展的框架,Xcode 应该会警告您。

许可

指令遵循 MIT 许可发布。请参阅 LICENSE 获取详细信息。