[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-6840":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":16,"stars7d":17,"stars30d":18,"stars90d":16,"forks30d":16,"starsTrendScore":16,"compositeScore":19,"rankGlobal":10,"rankLanguage":10,"license":20,"archived":21,"fork":21,"defaultBranch":22,"hasWiki":21,"hasPages":21,"topics":23,"createdAt":10,"pushedAt":10,"updatedAt":27,"readmeContent":28,"aiSummary":29,"trendingCount":16,"starSnapshotCount":16,"syncStatus":18,"lastSyncTime":30,"discoverSource":31},6840,"Instructions","ephread\u002FInstructions","ephread","Create walkthroughs and guided tours (coach marks) in a simple way, with Swift.","",null,"Swift",5195,480,75,12,0,1,2,39.05,"MIT License",false,"main",[24,25,26],"coach-marks","tour","walkthrough","2026-06-12 02:01:30","# ![Instructions](https:\u002F\u002Fi.imgur.com\u002F0WFRs8e.png)\n\n[![Build status](https:\u002F\u002Fgithub.com\u002Fephread\u002FInstructions\u002Fworkflows\u002Fbuild\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fephread\u002FInstructions\u002Factions?query=workflow%3Abuild)\n[![Maintainability](https:\u002F\u002Fimg.shields.io\u002Fcodeclimate\u002Fmaintainability\u002Fephread\u002FInstructions)](https:\u002F\u002Fcodeclimate.com\u002Fgithub\u002Fephread\u002FInstructions)\n[![Coverage](https:\u002F\u002Fimg.shields.io\u002Fcodeclimate\u002Fcoverage\u002Fephread\u002FInstructions)](https:\u002F\u002Fcodeclimate.com\u002Fgithub\u002Fephread\u002FInstructions)\n[![CocoaPods Shield](https:\u002F\u002Fimg.shields.io\u002Fcocoapods\u002Fv\u002FInstructions.svg)](https:\u002F\u002Fcocoapods.org\u002Fpods\u002FInstructions)\n[![Carthage compatible](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fcarthage-compatible-4BC51D.svg?style=flat)](https:\u002F\u002Fgithub.com\u002FCarthage\u002FCarthage)\n\nAdd customisable coach marks to your iOS project. Available for both iPhone and iPad.\n\n> [!IMPORTANT]\n> **MESSAGE FROM THE MAINTAINER**\n> \n> _Instructions_ is now considered _deprecated_. I will still fix issues, maintain compatibility with newer versions of Xcode\u002FiOS and accept maintenance-oriented Pull Requests, but no new features should be expected. If you can, migrate to SwiftUI and take advantage of [TipKit](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Ftipkit).\n\n# Table of contents\n\n  * [Overview](#overview)\n  * [Features](#features)\n  * [Requirements](#requirements)\n  * [Asking Questions \u002F Contributing](#asking-questions--contributing)\n      * [Asking Questions](#asking-questions)\n      * [Contributing](#contributing)\n  * [Installation](#installation)\n      * [CocoaPods](#cocoapods)\n      * [Carthage](#carthage)\n      * [Manually](#manually)\n  * [Usage](#usage)\n      * [Getting Started](#getting-started)\n      * [Advanced Usage](#advanced-usage)\n  * [Usage within App Extensions](#instructions-within-app-extensions)\n  * [License](#license)\n\n## Overview\n![Instructions Demo](https:\u002F\u002Fi.imgur.com\u002FZYBQQtt.gif)\n\n## Features\n- [x] [Customizable highlight system](#advanced-usage)\n- [x] [Customizable views](#providing-custom-views)\n- [x] [Customizable positions](#customizing-how-the-coach-mark-will-show)\n- [x] [Skippable tour](#let-users-skip-the-tour)\n- [x] [Pilotable from code](#piloting-the-flow-from-the-code)\n- [x] [App Extensions support](#usage-within-app-extensions)\n- [x] [Animatable coach marks](#animating-coach-marks)\n- [x] Right-to-left support\n- [x] Size transition support (orientation and multi-tasking)\n- [x] Partial `UIVisualEffectView` support\n- [ ] Cross controllers walkthrough\n- [ ] Multiple coach marks support\n\n## Requirements\n- Xcode 13 \u002F Swift 5+\n- iOS 14.0+\n\n## Asking Questions \u002F Contributing\n\n### Asking questions\n\nIf you need help with something, ask a question in the [Gitter room](https:\u002F\u002Fgitter.im\u002Fephread\u002FInstructions).\n\n### Contributing\n\nIf you want to contribute, look at [the contributing guide].\n\n[the contributing guide]: https:\u002F\u002Fgithub.com\u002Fephread\u002FInstructions\u002Fblob\u002Fmain\u002FCONTRIBUTING.md\n\n## Installation\n\n### CocoaPods\nAdd Instructions to your Podfile:\n\n```ruby\nsource 'https:\u002F\u002Fgithub.com\u002FCocoaPods\u002FSpecs.git'\n# Instructions is only supported for iOS 13+, but it\n# can be used on older versions at your own risk,\n# going as far back as iOS 9.\nplatform :ios, '9.0'\nuse_frameworks!\n\npod 'Instructions', '~> 2.3.0'\n```\n\nThen, run the following command:\n\n```bash\n$ pod install\n```\n\n### Carthage\nAdd Instructions to your Cartfile:\n\n```\ngithub \"ephread\u002FInstructions\" ~> 2.3.0\n```\n\nYou can then update, build and drag the generated framework into your project:\n\n```bash\n$ carthage update\n$ carthage build\n```\n\n### Swift Package Manager\nIn Xcode, use File > Swift Packages > Add Package Dependency and use `https:\u002F\u002Fgithub.com\u002Fephread\u002FInstructions`.\n\n### Manually\nIf you would rather stay away from both CocoaPods and Carthage, you can install Instructions manually, with the cost of managing updates yourself.\n\n#### Embedded Framework\n1. Drag the Instructions.xcodeproj into the project navigator of your application's Xcode project.\n2. Still in the project navigator, select your application project. The target configuration panel should show up.\n3. Select the appropriate target and in the \"General\" panel, scroll down to the \"Embedded Binaries\" section.\n4. Click on the + button and select the \"Instructions.framework\" under the \"Product\" directory.\n\n## Usage\n\n### Getting started\nOpen up the controller for which you wish to display coach marks and instantiate a new `CoachMarksController`. You should also provide a `dataSource`, an object conforming to the `CoachMarksControllerDataSource` protocol.\n\n```swift\nclass DefaultViewController: UIViewController,\n                             CoachMarksControllerDataSource,\n                             CoachMarksControllerDelegate {\n    let coachMarksController = CoachMarksController()\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        self.coachMarksController.dataSource = self\n    }\n}\n```\n\n#### Data Source\n`CoachMarksControllerDataSource` declares three mandatory methods.\n\nThe first one asks for the number of coach marks to display. Let's pretend that you want to show only one coach mark. Note that the `CoachMarksController` requesting the information is supplied, allowing you to provide data for multiple `CoachMarksController`, within a single data source.\n\n```swift\nfunc numberOfCoachMarks(for coachMarksController: CoachMarksController) -> Int {\n    return 1\n}\n```\n\nThe second one asks for metadata. This allows you to customise how a coach mark will position and appear but won't let you define its look (more on this later). Metadata is packaged in a struct named `CoachMark`. Note the parameter `coachMarkAt` that gives you the coach mark logical position, much like an `IndexPath` would do. `coachMarksController` provides you with an easy way to create a default `CoachMark` object from a given view.\n\n```swift\nlet pointOfInterest = UIView()\n\nfunc coachMarksController(_ coachMarksController: CoachMarksController,\n                          coachMarkAt index: Int) -> CoachMark {\n    return coachMarksController.helper.makeCoachMark(for: pointOfInterest)\n}\n```\n\nThe third one supplies two views (much like `cellForRowAtIndexPath`) in the form of a Tuple. The _body_ view is mandatory, as it's the core of the coach mark. The _arrow_ view is optional.\n\nBut for now, let's just return the default views provided by Instructions.\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    coachMarkViewsAt index: Int,\n    madeFrom coachMark: CoachMark\n) -> (bodyView: UIView & CoachMarkBodyView, arrowView: (UIView & CoachMarkArrowView)?) {\n    let coachViews = coachMarksController.helper.makeDefaultCoachViews(\n        withArrow: true,\n        arrowOrientation: coachMark.arrowOrientation\n    )\n\n    coachViews.bodyView.hintLabel.text = \"Hello! I'm a Coach Mark!\"\n    coachViews.bodyView.nextLabel.text = \"Ok!\"\n\n    return (bodyView: coachViews.bodyView, arrowView: coachViews.arrowView)\n}\n```\n\n#### Starting the coach marks flow\nOnce the `dataSource` is set up, you can start displaying the coach marks. You will most likely supply `self` to `start`. While the overlay adds itself as a child of the current window (to be on top of everything), the `CoachMarksController` will add itself as a child of the view controller you provide. The `CoachMarksController` will receive size change events and react accordingly. Be careful; you can't call `start` in the `viewDidLoad` method since the view hierarchy has to be set up and ready for Instructions to work correctly.\n\n```swift\noverride func viewDidAppear(_ animated: Bool) {\n    super.viewDidAppear(animated)\n\n    self.coachMarksController.start(in: .window(over: self))\n}\n```\n\n#### Stopping the coach marks flow\nYou should always stop the flow once the view disappears. To avoid animation artefacts and timing issues, don't forget to add the following code to your `viewWillDisappear` method. Calling `stop(immediately: true)` will ensure that the flow is stopped immediately upon the disappearance of the view.\n\n```swift\noverride func viewWillDisappear(_ animated: Bool) {\n    super.viewWillDisappear(animated)\n\n    self.coachMarksController.stop(immediately: true)\n}\n```\n\nYou're all set. You can check the `Examples\u002F` directory provided with the library for more examples.\n\n### Advanced Usage\n\n#### Customizing the overlay\nYou can customise the background colour of the overlay using this property:\n\n- `overlay.backgroundColor`\n\nYou can also make the overlay blur the content sitting behind it. Setting this property to anything else than `nil` will disable the `overlay.backgroundColor`:\n\n- `overlay.blurEffectStyle: UIBlurEffectStyle?`\n\nYou can make the overlay tappable. A tap on the overlay will hide the current coach mark and display the next one.\n\n- `overlay.isUserInteractionEnabled: Bool`\n\nYou can also allow touch events to be forwarded to the UIView underneath if they happen inside the cutout path…\n\n- `overlay.isUserInteractionEnabledInsideCutoutPath: Bool`\n\n…or you can ask the entire overlay to forward touch events to the views under.\n\n- `overlay.areTouchEventsForwarded: Bool`\n\n> **Warning**\n> The blurring overlay is not supported in app extensions.\n\n#### Customizing default coach marks\nThe default coach marks provide minimum customisation options.\n\n**Available in both `CoachMarkBodyDefaultView` and `CoachMarkArrowDefaultView`:**\n- `background.innerColor: UIColor`: the background color of the coachmark.\n- `background.borderColor: UIColor`: the border color of the coachmark.\n- `background.highlightedInnerColor: UIColor`: the background colour of the coach mark when the coach mark is highlighted.\n- `background.highlightedBorderColor: UIColor`: the border colour of the coach mark when the coach mark is highlighted.\n\n**Available only on `CoachMarkArrowDefaultView`:**\n\n- `background.cornerRadius: UIColor`: the corner radius of the coach mark.\n\nYou can also customise properties on `CoachMarkBodyDefaultView.hintLabel` and `CoachMarkBodyDefaultView.nextLabel`. For instance, you can change the position of `nextLabel` in the coach mark:\n\n```swift\nlet coachViews = coachMarksController.helper.makeDefaultCoachViews(\n    withArrow: true,\n    arrowOrientation: coachMark.arrowOrientation\n    nextLabelPosition: .topTrailing\n)\n\ncoachViews.bodyView.hintLabel.text = \"Hello! I'm a Coach Mark!\"\ncoachViews.bodyView.nextLabel.text = \"Ok!\"\n```\n\nRefer to `MixedCoachMarksViewsViewController.swift` and `NextPositionViewController.swift` for a practical example.\n\n#### Providing custom views\nIf the default customisation options are not enough, you can provide your custom views. A coach mark comprises a _body_ view and an _arrow_ view. Note that the term _arrow_ might be misleading. It doesn't have to be an actual arrow; it can be anything you want.\n\nA _body_ view must conform to the `CoachMarkBodyView` protocol. An _arrow_ view must conform to the `CoachMarkArrowView` protocol. Both of them must also be subclasses of `UIView`.\n\nReturning a `CoachMarkBodyView` view is mandatory, while returning a `CoachMarkArrowView` is optional.\n\n##### CoachMarkBodyView Protocol #####\nThis protocol defines two properties.\n\n- `nextControl: UIControl? { get }` you must implement a getter method for this property in your view; this will let the `CoachMarkController` know which control should be tapped to display the next coach mark. Note that it doesn't have to be a subview; you can return the view itself.\n\n- `highlightArrowDelegate: CoachMarkBodyHighlightArrowDelegate?` If the view itself is the control receiving taps, you might want to forward its highlight state to the _arrow_ view (so they can look like the same component). The `CoachMarkController` will automatically set an appropriate delegate to this property. You'll then be able to do this:\n\n```swift\noverride var highlighted: Bool {\n    didSet {\n        self.highlightArrowDelegate?.highlightArrow(self.highlighted)\n    }\n}\n```\n\n##### Taking orientation into account #####\nRemember the following method from the dataSource?\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    coachMarkViewsAt index: Int,\n    madeFrom coachMark: CoachMark\n) -> (bodyView: UIView & CoachMarkBodyView, arrowView: (UIView & CoachMarkArrowView)?) {\n    let coachViews = coachMarksController.helper.makeDefaultCoachViews(\n        withArrow: true,\n        arrowOrientation: coachMark.arrowOrientation\n    )\n}\n```\n\nWhen providing a customised view, you need to give an _arrow_ view with the appropriate orientation (i. e. in the case of an actual arrow, pointing upward or downward). The `CoachMarkController` will tell you which orientation it expects through the following property: `CoachMark.arrowOrientation`.\n\nBrowse the `Example\u002F` directory for more details.\n\n#### Providing a custom cutout path\nIf you dislike how the default cutout path looks like, you can customise it by providing a block to `makeCoachMark(for:)`. The cutout path will automatically be stored in the `cutoutPath` property of the returning `CoachMark` object:\n\n```swift\nvar coachMark = coachMarksController.helper.makeCoachMark(\n    for: customView,\n    cutoutPathMaker: { (frame: CGRect) -> UIBezierPath in\n        \u002F\u002F This will create an oval cutout a bit larger than the view.\n        return UIBezierPath(ovalIn: frame.insetBy(dx: -4, dy: -4))\n    }\n)\n```\n\n`frame` is the frame of `customView` expressed in the coordinate space of `coachMarksController.view`.\nThe conversion between this coordinate space and Instructions' coordinate space is handled automatically.\nAny shape can be provided, from a simple rectangle to a complex star.\n\nYou can also pass a frame rectangle directly if you supply its coordinate space.\n\n```swift\nvar coachMark = coachMarksController.helper.makeCoachMark(\n    forFrame: frame,\n    in: superview,\n    cutoutPathMaker: { (frame: CGRect) -> UIBezierPath in\n        \u002F\u002F This will create an oval cutout a bit larger than the view.\n        return UIBezierPath(ovalIn: frame.insetBy(dx: -4, dy: -4))\n    }\n)\n```\n\n#### Presentation Context\n\nYou can choose in which context the coach marks will be displayed, by passing it to `start(in: PresentationContext). The available contexts are:\n\n- `.newWindow(over: UIViewController, at: UIWindowLevel?)` – A new window created at the given `UIWindowLevel` (not available in app extensions);\n- `.currentWindow(of: UIViewController)` – The window displaying the given `UIViewController`;\n- `.viewController(_: UIViewController)` – In the `view` of the given `UIViewController`.\n\nAdditionally, you can also provide use `window(over: UIViewController)`, which is a convenience static method equivalent to calling `.newWindow(over: UIViewController, at: UIWindowLevelNormal + 1)`.\n\n> **Warning**\n> Setting the window level to anything above `UIWindowLevelStatusBar` is not supported on iOS 13+ or when adding a blur effect on the overlay.\n\nWhen the coach marks are displayed in a `. newWindow` context, the custom window is exposed by `CoachMarkController` through the `rootWindow` property.\n\n#### Customizing how the coach mark will show\nYou can customise the following properties:\n\n- `gapBetweenBodyAndArrow: CGFloat`: the vertical gap between the _body_ and the _arrow_ in a given coach mark.\n\n- `pointOfInterest: CGPoint?`: the point toward which the arrow will face. At the moment, it's only used to shift the arrow horizontally and make it sits above or below the point of interest.\n\n- `gapBetweenCoachMarkAndCutoutPath: CGFloat`: the gap between the coach mark and the cutout path.\n\n- `maxWidth: CGFloat`: the maximum width a coach mark can take. You don't want your coach marks to be too wide, especially on iPads.\n\n- `horizontalMargin: CGFloat` is the margin (both leading and trailing) between the edges of the overlay view and the coach mark. Note that if the max-width of your coach mark is less than the width of the overlay view, your view will either stack on the left or the right, leaving space on the other side.\n\n- `arrowOrientation: CoachMarkArrowOrientation?` is the orientation of the arrow (not the coach mark, meaning setting this property to `.Top` will display the coach mark below the point of interest). Although it's usually pre-computed by the library, you can override it in `coachMarksForIndex:` or in `coachMarkWillShow:`.\n\n- `isDisplayedOverCutoutPath: Bool` enables the coach mark to be displayed over the cutout path; please note that arrows won't be visible if you set this property to `true`.\n\n- `isOverlayInteractionEnabled: Bool` is used to disable the ability to tap on the overlay to show the next coach mark on a case-by-case basis; it defaults to `true`.\n\n- `isUserInteractionEnabledInsideCutoutPath: Bool` is used to allow touch forwarding inside the cutout path. Take a look at `TransitionFromCodeViewController`, in the `Example\u002F` directory, for more information.\n\n#### Animating coach marks\nTo animates coach marks, you will need to implement the `CoachMarksControllerAnimationDelegate` protocol.\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    fetchAppearanceTransitionOfCoachMark coachMarkView: UIView,\n    at index: Int,\n    using manager: CoachMarkTransitionManager\n)\n\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    fetchDisappearanceTransitionOfCoachMark coachMarkView: UIView,\n    at index: Int,\n    using manager: CoachMarkTransitionManager\n)\n\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    fetchIdleAnimationOfCoachMark coachMarkView: UIView,\n    at index: Int,\n    using manager: CoachMarkAnimationManager\n)\n```\n\nAll methods from this delegate work in similar ways. First, you will need to specify the general parameters of the animation via `manager.parameters` properties. These properties match the configuration parameters you can provide to `UIView.animate`.\n\n- `duration: TimeInterval`: the total duration of the animation.\n\n- `delay: TimeInterval`: the amount of time to wait before beginning the animations\n\n- `options: UIViewAnimationOptions`: a mask of options indicating how you want to perform the animations (for regular animations).\n\n- `keyframeOptions: UIViewKeyframeAnimationOptions`: a mask of options indicating how you want to perform the animations (for keyframe animations).\n\nOnce you've set the parameters, you should provide your animations by calling `manager.animate`. The method signature is different, whether you are animating the idle state of coach marks or making them appear\u002Fdisappear.\n\nYou should provide your animations in a block passed to the `animate` parameter, similarly to `UIView.animate`. If you need to access the animation parameters or the coach mark metadata, a `CoachMarkAnimationManagementContext` containing these will be provided to your animation block. You shouldn't capture a reference to the manager from the animation block.\n\nFor an implementation example, you can also look at the `DelegateViewController` class found in the `Example` directory.\n\n##### Appearance and disappearance specifics\nIf you need to define an initial state, you should provide a block to the `fromInitialState` property. While directly setting values on `coachMarkView` in the method before calling `manager.animate()` might work, it's not guaranteed to.\n\n#### Let users skip the tour\n##### Control\nYou can provide the user with a means to skip the coach marks. First, you will need to set\n`skipView` with a `UIView` conforming to the `CoachMarkSkipView` protocol. This protocol defines a single property:\n\n```swift\npublic protocol CoachMarkSkipView: AnyObject {\n    var skipControl: UIControl? { get }\n}\n```\n\nYou must implement a getter method for this property in your view. This will let the `CoachMarkController` know which control should be tapped to skip the tour. Again, it doesn't have to be a subview; you can return the view itself.\n\nAs usual, Instructions provides a default implementation of `CoachMarkSkipView` named `CoachMarkSkipDefaultView`.\n\n##### dataSource\nTo define how the view will position itself, you can use a method from the `CoachMarkControllerDataSource` protocol. This method is optional.\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    constraintsForSkipView skipView: UIView,\n    inParent parentView: UIView\n) -> [NSLayoutConstraint]?\n```\n\nThis method will be called by the `CoachMarksController` before starting the tour and whenever there is a size change. It gives you the _skip button_ and the view in which it will be positioned and expects an array of `NSLayoutConstraints` in return. These constraints will define how the _skip button_ will be placed in its parent. You should not add the constraints yourself; just return them.\n\nReturning `nil` will tell the `CoachMarksController` to use the default constraints, which will position the _skip button_ at the top of the screen. Returning an empty array is discouraged, as it will probably lead to an awkward positioning.\n\nYou can check the `Example\u002F` directory for more information about the skip mechanism.\n\n#### Piloting the flow from the code\nShould you ever need to programmatically show the coach mark, `CoachMarkController.flow` also provides the following methods:\n\n```swift\nfunc showNext(numberOfCoachMarksToSkip numberToSkip: Int = 0)\n```\n\n```swift\nfunc showPrevious(numberOfCoachMarksToSkip numberToSkip: Int = 0)\n```\n\nYou can specify the number of coach marks to skip (jumping forward or backwards to a different index).\n\nTake a look at `TransitionFromCodeViewController` in the `Example\u002F` directory to see how you can leverage this method to ask the user to perform specific actions.\n\n#### Using a delegate\nThe `CoachMarkController` will notify the delegate on multiple occasions. All those methods are optional.\n\nFirst, when a coach mark shows. You might want to change something about the view. For that reason, the `CoachMark` metadata structure is passed as an `inout` object so that you can update it with new parameters.\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    willShow coachMark: inout CoachMark,\n    at index: Int\n)\n```\n\nSecond, when a coach mark disappears.\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    willHide coachMark: CoachMark,\n    at index: Int\n)\n```\n\nThird, when all coach marks have been displayed. `didEndShowingBySkipping` specify whether the flow was completed because the user requested it to end.\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    didEndShowingBySkipping skipped: Bool\n)\n```\n\n##### React when the user tap the overlay #####\n\nWhenever the user will tap the overlay, you will get notified through:\n\n```swift\nfunc shouldHandleOverlayTap(\n    in coachMarksController: CoachMarksController,\n    at index: Int\n) -> Bool\n```\n\nReturning `true` will let Instructions continue the flow typically while returning `false` will interrupt it. If you choose to interrupt the flow, you're responsible for either stopping or pausing it or manually showing the next coach marks (see [Piloting the flow from the code](#piloting-the-flow-from-the-code)).\n\n`index` is the index of the coach mark currently displayed.\n\n##### Pausing and resuming the flow\nIt's as simple as calling `coachMarksController.flow.pause()` and `coachMarksController.flow.resume()`. While pausing, you can also choose to hide Instructions's overlay altogether (`.pause(and: hideInstructions)`), or only hide the overlay and retain its touch blocking capabilities (`.pause(and: hideOverlay)`).\n\n##### Performing animations before showing coach marks #####\nYou can perform animations on views before or after showing a coach mark.\nFor instance, you might want to collapse a table view and show only its header before referring to those headers with a coach mark. Instructions offers a simple way to insert your animations into the flow.\n\nFor instance, let's say you want to perform an animation _before_ a coach mark shows.\nYou'll implement some logic into the `coachMarkWillShow` delegate method.\nTo ensure you don't have to hack something up and turn asynchronous animation blocks into synchronous ones, you can pause the flow, perform the animation and then start the flow again. This will ensure your UI never gets stalled.\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    willShow coachMark: inout CoachMark,\n    at index: Int\n) {\n    \u002F\u002F Pause to be able to play the animation and then show the coach mark.\n    coachMarksController.flow.pause()\n\n    \u002F\u002F Run the animation\n    UIView.animateWithDuration(1, animations: { () -> Void in\n        …\n    }, completion: { (finished: Bool) -> Void in\n        \u002F\u002F Once the animation is completed, we update the coach mark,\n        \u002F\u002F and start the display again. Since inout parameters cannot be\n        \u002F\u002F captured by the closure, you can use the following method to update\n        \u002F\u002F the coach mark. It will only work if you paused the flow.\n        coachMarksController.helper.updateCurrentCoachMark(using: myView)\n        coachMarksController.flow.resume()\n    })\n}\n```\nIf you need to update multiple properties on the coach mark, you may prefer the block-based method.\nWhen updating points of interest and cutout paths, express them in Instructions' coordinate\nspace using the provided converter.\n\n```\ncoachMarksController.helper.updateCurrentCoachMark { coachMark, converter in\n    coachMark.pointOfInterest = converter.convert(point: myPoint, from: myPointSuperview)\n    coachMark.gapBetweenCoachMarkAndCutoutPath = 6\n}\n```\n\n> **Warning**\n> Since the blurring overlay snapshots the view during coach mark appearance\u002Fdisappearance, you should make sure that animations targeting your view don't occur while a coach mark appears or disappears. Otherwise, the animation won't be visible.\n\nYou may also want to customise the classic transparency overlay, as Instructions will fall back to using the traditional type if `UIAccessibility.isReduceTransparencyEnabled` returns true.\n\n##### Skipping a coach mark\n\nYou can skip a given coach mark by implementing the following method defined in `CoachMarksControllerDelegate`:\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    coachMarkWillLoadAt index: Int\n) -> Bool\n```\n\n`coachMarkWillLoadAt:` is called right before a given coach mark will show. To prevent a CoachMark from showing, you can return `false` from this method.\n\n##### Customizing ornaments of the overlay\n\nIt's possible to add custom views which will be displayed over the overlay by implementing the following method of `CoachMarksControllerDelegate`:\n\n```swift\nfunc coachMarksController(\n    _ coachMarksController: CoachMarksController,\n    configureOrnamentsOfOverlay overlay: UIView\n)\n```\n\nJust add the ornaments to the provided view (`overlay`), and Instructions should take care of the rest. Please note, however, that these ornaments will be displayed over the cutout but under the coach marks.\n\n#### Dealing with frame changes\n\nSince Instructions doesn't hold any reference to the _views of interest_, it cannot respond to their\nchange of frame automatically.\n\nInstructions provide two methods to deal with frame changes.\n\n- `CoachMarkController.prepareForChange()`, called before a frame change, to hide\n  the coach mark and the cutout path.\n- `CoachMarkController.restoreAfterChangeDidComplete()`, called after a change of frame\n  to show the coach mark and the cutout again.\n\nAlthough you can call these methods at any time while Instructions is idle, the result will not\nlook smooth if the coach mark is already displayed. It's better to perform the changes between\ntwo coach marks by pausing and resuming the flow. [`KeyboardViewController`] shows an\nexample of this technique.\n\n[`KeyboardViewController`]: https:\u002F\u002Fgithub.com\u002Fephread\u002FInstructions\u002Fblob\u002Fmain\u002FExamples\u002FExample\u002FKeyboardViewController.swift\n\n### Usage within App Extensions\nIf you wish to add Instructions within App Extensions, there's additional work you need to perform.\nAn example is available in the `App Extensions Example\u002F` directory.\n\n#### Dependencies\nInstructions comes with two shared schemes, `Instructions` and `InstructionsAppExtensions`. The only difference between the two is that `InstructionsAppExtensions` does not depend upon the `UIApplication.sharedApplication()`, making it suitable for App Extensions.\n\nIn the following examples, let's consider a project with two targets, one for a regular application (`Instructions App Extensions Example`) and another for an app extension (`Keyboard Extension`).\n\n#### CocoaPods\n\nIf you're importing Instructions with CocoaPods, you'll need to edit your `Podfile` to make it look\nlike this:\n\n```ruby\ntarget 'Instructions App Extensions Example' do\n  pod 'Instructions', '~> 2.3.0'\nend\n\ntarget 'Keyboard Extension' do\n  pod 'InstructionsAppExtensions', '~> 2.3.0'\nend\n```\n\nIf Instructions is only imported from within the App Extension target, you don't need the first block.\n\nWhen compiling either target, CocoaPods will make sure the appropriate flags are set, thus\nallowing\u002Fforbidding calls to `UIApplication.sharedApplication()`.\nYou don't need to change your code.\n\n#### Frameworks (Carthage \u002F Manual management)\n\nIf you're importing Instructions through frameworks, you'll notice the two shared schemes.\n(`Instructions` and `InstructionsAppExtensions`) both result in different frameworks.\n\nYou need to embed both frameworks and link them to the proper targets.\nMake sure they look like these:\n\n**Instructions App Extensions Example**\n![Imgur](http:\u002F\u002Fi.imgur.com\u002F3M3BQaO.png)\n\n**Keyboard Extension**\n![Imgur](http:\u002F\u002Fi.imgur.com\u002FLAtV0oA.png)\n\nIf you plan to add Instructions only to the App Extension target, you don't need to add `Instructions.frameworks`.\n\n##### Import statements\n\nWhen importing Instructions from files within `Instructions App Extensions Example`,\nyou should use the regular import statement:\n\n```swift\nimport Instructions\n```\n\nHowever, when importing Instructions from files within `Keyboard Extension`, you should\nuse the specific statement:\n\n```swift\nimport InstructionsAppExtensions\n```\n\n> **Warning**\n> it's possible to import _Instructions_ in an app extension. However, you're at a high risk of rejection from the Apple Store.\nUses of `UIApplication.sharedApplication()` are statically checked during compilation, but nothing prevents you from performing the calls at runtime. Fortunately, Xcode should warn you if you've mistakenly linked with a framework not suited for App Extensions.\n\n## License\n\nInstructions is released under the MIT license. See LICENSE for details.\n","Instructions 是一个用于在iOS应用中轻松创建引导教程和教练标记的Swift库。它支持高度自定义的高亮系统、视图样式及位置，并允许用户跳过教程，同时可以从代码层面控制流程。此外，还提供了对动画效果的支持以及右至左语言环境下的适配能力。适用于需要向用户介绍新功能或复杂界面操作的应用场景，特别是在引入了新特性或重新设计了用户界面后帮助用户快速上手上。虽然项目已不再积极开发新特性，但仍保持与最新Xcode\u002FiOS版本的兼容性维护。","2026-06-11 03:09:10","top_language"]