InstructionsAppExtensions 2.3.0

InstructionsAppExtensions 2.3.0

测试测试
语言语言 SwiftSwift
许可证 MIT
发布最新发布2023年9月
SPM支持SPM

Frédéric Maquin维护。



  • 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)
}

第三个方法以元组的形式提供两个视图(类似于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

警告阴影覆盖层在应用扩展中不受支持。

自定义默认教练标记

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

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

framecustomViewcoachMarksController.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) – 在给定 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 属性。虽然在前一个方法中直接在 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)

您可以指定要跳过的教练标记数量(跳转到不同的索引)。

请查看 TransitionFromCodeViewControllerExample/ 目录下的代码,了解如何利用该方法让用户执行特定的操作。

使用代理

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包含两个共享计划,分别是InstructionsInstructionsAppExtensions。这两个计划的唯一区别在于,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 / 手动管理)

如果您通过框架导入指令,会发现有两种共享方案。(InstructionsInstructionsAppExtensions)它们都会导致不同的框架。

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

Instructions App Extensions 示例 Imgur

键盘扩展 Imgur

如果您计划只将指令添加到 App 扩展目标,则不需要添加 Instructions.frameworks

导入语句

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

import Instructions

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

import InstructionsAppExtensions

警告在应用扩展中导入 Instructions 是可能的。但是,您有很大的风险被 Apple Store 拒绝。使用 UIApplication.sharedApplication() 的应用是在编译时进行了静态检查,但在运行时没有阻止您进行调用。幸运的是,Xcode 应该会警告您是否错误地链接了不适用于应用扩展的框架。

许可证

Instructions 是在 MIT 许可证下发布的。详细资料请见 LICENSE 文件。