├── .gitignore ├── AppGuideOverlay.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── AppGuideOverlay.xcscheme ├── AppGuideOverlay ├── AppGuide.swift ├── AppGuideOverlay.h ├── AppGuideOverlay.swift ├── AppGuidePresenter.swift ├── AppGuideViewController.swift ├── DisplaysAppGuide.swift ├── Images │ ├── finishTemplate.pdf │ ├── nextTemplate.pdf │ └── prevTemplate.pdf ├── Info.plist ├── LoopingAnimation │ ├── AnimationLoop.swift │ ├── SmoothAnimation.swift │ └── ValueAnimationLoop.swift ├── NSTextField+Label.swift ├── NSView+constraints.swift ├── OverlainWindow.swift ├── OverlayButton.swift ├── OverlayButtonLabels.swift ├── OverlayLabelView.swift ├── OverlayLabelViewController.swift ├── OverlayView.swift └── OverlayViewController.swift ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── MainMenu.xib ├── Example.entitlements └── Info.plist ├── LICENSE ├── README.md └── img ├── auto-layout.gif └── breathing.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /AppGuideOverlay.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 500F2B2823D30492002B7BD1 /* OverlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F2B2723D30492002B7BD1 /* OverlayButton.swift */; }; 11 | 500F5BC9201CBCA700B78822 /* AppGuideOverlay.h in Headers */ = {isa = PBXBuildFile; fileRef = 500F5BC7201CBCA700B78822 /* AppGuideOverlay.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 500F5BDA201CBD7A00B78822 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BD9201CBD7A00B78822 /* AppDelegate.swift */; }; 13 | 500F5BDC201CBD7A00B78822 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 500F5BDB201CBD7A00B78822 /* Assets.xcassets */; }; 14 | 500F5BDF201CBD7A00B78822 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 500F5BDD201CBD7A00B78822 /* MainMenu.xib */; }; 15 | 500F5BE6201CBDA300B78822 /* OverlainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BE5201CBDA300B78822 /* OverlainWindow.swift */; }; 16 | 500F5BE8201CBE2200B78822 /* AppGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BE7201CBE2200B78822 /* AppGuide.swift */; }; 17 | 500F5BEA201CBF3400B78822 /* AppGuidePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BE9201CBF3400B78822 /* AppGuidePresenter.swift */; }; 18 | 500F5BEC201CBF6F00B78822 /* AppGuideViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BEB201CBF6F00B78822 /* AppGuideViewController.swift */; }; 19 | 500F5BEE201DD18C00B78822 /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BED201DD18C00B78822 /* OverlayView.swift */; }; 20 | 500F5BF0201DD1A900B78822 /* OverlayLabelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BEF201DD1A900B78822 /* OverlayLabelViewController.swift */; }; 21 | 500F5BF2201DD1BE00B78822 /* OverlayLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BF1201DD1BE00B78822 /* OverlayLabelView.swift */; }; 22 | 500F5BF4201DD1D600B78822 /* OverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BF3201DD1D600B78822 /* OverlayViewController.swift */; }; 23 | 500F5BF6201DD27A00B78822 /* NSView+constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BF5201DD27A00B78822 /* NSView+constraints.swift */; }; 24 | 500F5BF8201DD2A700B78822 /* NSTextField+Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BF7201DD2A700B78822 /* NSTextField+Label.swift */; }; 25 | 500F5BFE201DD3B500B78822 /* ValueAnimationLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BFA201DD3B500B78822 /* ValueAnimationLoop.swift */; }; 26 | 500F5BFF201DD3B500B78822 /* SmoothAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BFB201DD3B500B78822 /* SmoothAnimation.swift */; }; 27 | 500F5C01201DD3B500B78822 /* AnimationLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5BFD201DD3B500B78822 /* AnimationLoop.swift */; }; 28 | 500F5C03201DD4D000B78822 /* NSView+constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500F5C02201DD4D000B78822 /* NSView+constraints.swift */; }; 29 | 500F5C04201DD5D700B78822 /* AppGuideOverlay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 500F5BC4201CBCA700B78822 /* AppGuideOverlay.framework */; }; 30 | 500F5C05201DD5D700B78822 /* AppGuideOverlay.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 500F5BC4201CBCA700B78822 /* AppGuideOverlay.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 31 | 507CF1962025DD0B0048F133 /* OverlayButtonLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CF1952025DD0B0048F133 /* OverlayButtonLabels.swift */; }; 32 | 507CF198202632D80048F133 /* DisplaysAppGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CF197202632D80048F133 /* DisplaysAppGuide.swift */; }; 33 | 508876C520493F5500B0F748 /* finishTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 508876C420493F5500B0F748 /* finishTemplate.pdf */; }; 34 | 508876C720493F5900B0F748 /* nextTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 508876C620493F5900B0F748 /* nextTemplate.pdf */; }; 35 | 508876C920493F5E00B0F748 /* prevTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 508876C820493F5E00B0F748 /* prevTemplate.pdf */; }; 36 | 50F54CC12025C0EF00C419E4 /* AppGuideOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F54CC02025C0EF00C419E4 /* AppGuideOverlay.swift */; }; 37 | /* End PBXBuildFile section */ 38 | 39 | /* Begin PBXContainerItemProxy section */ 40 | 500F5C06201DD5D700B78822 /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = 500F5BBB201CBCA700B78822 /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = 500F5BC3201CBCA700B78822; 45 | remoteInfo = AppGuideOverlay; 46 | }; 47 | /* End PBXContainerItemProxy section */ 48 | 49 | /* Begin PBXCopyFilesBuildPhase section */ 50 | 500F5C08201DD5D700B78822 /* Embed Frameworks */ = { 51 | isa = PBXCopyFilesBuildPhase; 52 | buildActionMask = 2147483647; 53 | dstPath = ""; 54 | dstSubfolderSpec = 10; 55 | files = ( 56 | 500F5C05201DD5D700B78822 /* AppGuideOverlay.framework in Embed Frameworks */, 57 | ); 58 | name = "Embed Frameworks"; 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXCopyFilesBuildPhase section */ 62 | 63 | /* Begin PBXFileReference section */ 64 | 500F2B2723D30492002B7BD1 /* OverlayButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayButton.swift; sourceTree = ""; }; 65 | 500F5BC4201CBCA700B78822 /* AppGuideOverlay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppGuideOverlay.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | 500F5BC7201CBCA700B78822 /* AppGuideOverlay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppGuideOverlay.h; sourceTree = ""; }; 67 | 500F5BC8201CBCA700B78822 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 68 | 500F5BCF201CBD4900B78822 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 69 | 500F5BD0201CBD4900B78822 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 70 | 500F5BD7201CBD7A00B78822 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 71 | 500F5BD9201CBD7A00B78822 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 72 | 500F5BDB201CBD7A00B78822 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 73 | 500F5BDE201CBD7A00B78822 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 74 | 500F5BE0201CBD7A00B78822 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 75 | 500F5BE1201CBD7A00B78822 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; 76 | 500F5BE5201CBDA300B78822 /* OverlainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlainWindow.swift; sourceTree = ""; }; 77 | 500F5BE7201CBE2200B78822 /* AppGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGuide.swift; sourceTree = ""; }; 78 | 500F5BE9201CBF3400B78822 /* AppGuidePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGuidePresenter.swift; sourceTree = ""; }; 79 | 500F5BEB201CBF6F00B78822 /* AppGuideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGuideViewController.swift; sourceTree = ""; }; 80 | 500F5BED201DD18C00B78822 /* OverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = ""; }; 81 | 500F5BEF201DD1A900B78822 /* OverlayLabelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayLabelViewController.swift; sourceTree = ""; }; 82 | 500F5BF1201DD1BE00B78822 /* OverlayLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayLabelView.swift; sourceTree = ""; }; 83 | 500F5BF3201DD1D600B78822 /* OverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayViewController.swift; sourceTree = ""; }; 84 | 500F5BF5201DD27A00B78822 /* NSView+constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSView+constraints.swift"; sourceTree = ""; }; 85 | 500F5BF7201DD2A700B78822 /* NSTextField+Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+Label.swift"; sourceTree = ""; }; 86 | 500F5BFA201DD3B500B78822 /* ValueAnimationLoop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueAnimationLoop.swift; sourceTree = ""; }; 87 | 500F5BFB201DD3B500B78822 /* SmoothAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmoothAnimation.swift; sourceTree = ""; }; 88 | 500F5BFD201DD3B500B78822 /* AnimationLoop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationLoop.swift; sourceTree = ""; }; 89 | 500F5C02201DD4D000B78822 /* NSView+constraints.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "NSView+constraints.swift"; path = "AppGuideOverlay/NSView+constraints.swift"; sourceTree = SOURCE_ROOT; }; 90 | 507CF1952025DD0B0048F133 /* OverlayButtonLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayButtonLabels.swift; sourceTree = ""; }; 91 | 507CF197202632D80048F133 /* DisplaysAppGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaysAppGuide.swift; sourceTree = ""; }; 92 | 508876C420493F5500B0F748 /* finishTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = finishTemplate.pdf; sourceTree = ""; }; 93 | 508876C620493F5900B0F748 /* nextTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = nextTemplate.pdf; sourceTree = ""; }; 94 | 508876C820493F5E00B0F748 /* prevTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = prevTemplate.pdf; sourceTree = ""; }; 95 | 50F54CC02025C0EF00C419E4 /* AppGuideOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGuideOverlay.swift; sourceTree = ""; }; 96 | /* End PBXFileReference section */ 97 | 98 | /* Begin PBXFrameworksBuildPhase section */ 99 | 500F5BC0201CBCA700B78822 /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | 500F5BD4201CBD7A00B78822 /* Frameworks */ = { 107 | isa = PBXFrameworksBuildPhase; 108 | buildActionMask = 2147483647; 109 | files = ( 110 | 500F5C04201DD5D700B78822 /* AppGuideOverlay.framework in Frameworks */, 111 | ); 112 | runOnlyForDeploymentPostprocessing = 0; 113 | }; 114 | /* End PBXFrameworksBuildPhase section */ 115 | 116 | /* Begin PBXGroup section */ 117 | 500F5BBA201CBCA700B78822 = { 118 | isa = PBXGroup; 119 | children = ( 120 | 500F5BD0201CBD4900B78822 /* LICENSE */, 121 | 500F5BCF201CBD4900B78822 /* README.md */, 122 | 500F5BC6201CBCA700B78822 /* AppGuideOverlay */, 123 | 500F5BD8201CBD7A00B78822 /* Example */, 124 | 500F5BC5201CBCA700B78822 /* Products */, 125 | ); 126 | sourceTree = ""; 127 | }; 128 | 500F5BC5201CBCA700B78822 /* Products */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 500F5BC4201CBCA700B78822 /* AppGuideOverlay.framework */, 132 | 500F5BD7201CBD7A00B78822 /* Example.app */, 133 | ); 134 | name = Products; 135 | sourceTree = ""; 136 | }; 137 | 500F5BC6201CBCA700B78822 /* AppGuideOverlay */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 500F5BC7201CBCA700B78822 /* AppGuideOverlay.h */, 141 | 500F5BE5201CBDA300B78822 /* OverlainWindow.swift */, 142 | 50F54CC02025C0EF00C419E4 /* AppGuideOverlay.swift */, 143 | 500F5BE7201CBE2200B78822 /* AppGuide.swift */, 144 | 500F5BE9201CBF3400B78822 /* AppGuidePresenter.swift */, 145 | 507CF197202632D80048F133 /* DisplaysAppGuide.swift */, 146 | 500F5BEB201CBF6F00B78822 /* AppGuideViewController.swift */, 147 | 500F5BF3201DD1D600B78822 /* OverlayViewController.swift */, 148 | 500F5BED201DD18C00B78822 /* OverlayView.swift */, 149 | 500F5BEF201DD1A900B78822 /* OverlayLabelViewController.swift */, 150 | 500F5BF1201DD1BE00B78822 /* OverlayLabelView.swift */, 151 | 500F2B2723D30492002B7BD1 /* OverlayButton.swift */, 152 | 507CF1952025DD0B0048F133 /* OverlayButtonLabels.swift */, 153 | 500F5BF5201DD27A00B78822 /* NSView+constraints.swift */, 154 | 500F5BF7201DD2A700B78822 /* NSTextField+Label.swift */, 155 | 500F5BF9201DD3AB00B78822 /* LoopingAnimation */, 156 | 500F5BC8201CBCA700B78822 /* Info.plist */, 157 | 508876CA20493F9500B0F748 /* Images */, 158 | ); 159 | path = AppGuideOverlay; 160 | sourceTree = ""; 161 | }; 162 | 500F5BD8201CBD7A00B78822 /* Example */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 500F5BD9201CBD7A00B78822 /* AppDelegate.swift */, 166 | 500F5C02201DD4D000B78822 /* NSView+constraints.swift */, 167 | 500F5BDB201CBD7A00B78822 /* Assets.xcassets */, 168 | 500F5BDD201CBD7A00B78822 /* MainMenu.xib */, 169 | 500F5BE0201CBD7A00B78822 /* Info.plist */, 170 | 500F5BE1201CBD7A00B78822 /* Example.entitlements */, 171 | ); 172 | path = Example; 173 | sourceTree = ""; 174 | }; 175 | 500F5BF9201DD3AB00B78822 /* LoopingAnimation */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | 500F5BFD201DD3B500B78822 /* AnimationLoop.swift */, 179 | 500F5BFB201DD3B500B78822 /* SmoothAnimation.swift */, 180 | 500F5BFA201DD3B500B78822 /* ValueAnimationLoop.swift */, 181 | ); 182 | path = LoopingAnimation; 183 | sourceTree = ""; 184 | }; 185 | 508876CA20493F9500B0F748 /* Images */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 508876C420493F5500B0F748 /* finishTemplate.pdf */, 189 | 508876C620493F5900B0F748 /* nextTemplate.pdf */, 190 | 508876C820493F5E00B0F748 /* prevTemplate.pdf */, 191 | ); 192 | path = Images; 193 | sourceTree = ""; 194 | }; 195 | /* End PBXGroup section */ 196 | 197 | /* Begin PBXHeadersBuildPhase section */ 198 | 500F5BC1201CBCA700B78822 /* Headers */ = { 199 | isa = PBXHeadersBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | 500F5BC9201CBCA700B78822 /* AppGuideOverlay.h in Headers */, 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | /* End PBXHeadersBuildPhase section */ 207 | 208 | /* Begin PBXNativeTarget section */ 209 | 500F5BC3201CBCA700B78822 /* AppGuideOverlay */ = { 210 | isa = PBXNativeTarget; 211 | buildConfigurationList = 500F5BCC201CBCA700B78822 /* Build configuration list for PBXNativeTarget "AppGuideOverlay" */; 212 | buildPhases = ( 213 | 500F5BBF201CBCA700B78822 /* Sources */, 214 | 500F5BC0201CBCA700B78822 /* Frameworks */, 215 | 500F5BC1201CBCA700B78822 /* Headers */, 216 | 500F5BC2201CBCA700B78822 /* Resources */, 217 | ); 218 | buildRules = ( 219 | ); 220 | dependencies = ( 221 | ); 222 | name = AppGuideOverlay; 223 | productName = AppGuideOverlay; 224 | productReference = 500F5BC4201CBCA700B78822 /* AppGuideOverlay.framework */; 225 | productType = "com.apple.product-type.framework"; 226 | }; 227 | 500F5BD6201CBD7A00B78822 /* Example */ = { 228 | isa = PBXNativeTarget; 229 | buildConfigurationList = 500F5BE2201CBD7A00B78822 /* Build configuration list for PBXNativeTarget "Example" */; 230 | buildPhases = ( 231 | 500F5BD3201CBD7A00B78822 /* Sources */, 232 | 500F5BD4201CBD7A00B78822 /* Frameworks */, 233 | 500F5BD5201CBD7A00B78822 /* Resources */, 234 | 500F5C08201DD5D700B78822 /* Embed Frameworks */, 235 | ); 236 | buildRules = ( 237 | ); 238 | dependencies = ( 239 | 500F5C07201DD5D700B78822 /* PBXTargetDependency */, 240 | ); 241 | name = Example; 242 | productName = Example; 243 | productReference = 500F5BD7201CBD7A00B78822 /* Example.app */; 244 | productType = "com.apple.product-type.application"; 245 | }; 246 | /* End PBXNativeTarget section */ 247 | 248 | /* Begin PBXProject section */ 249 | 500F5BBB201CBCA700B78822 /* Project object */ = { 250 | isa = PBXProject; 251 | attributes = { 252 | LastSwiftUpdateCheck = 0920; 253 | LastUpgradeCheck = 1420; 254 | ORGANIZATIONNAME = "Christian Tietze"; 255 | TargetAttributes = { 256 | 500F5BC3201CBCA700B78822 = { 257 | CreatedOnToolsVersion = 9.2; 258 | LastSwiftMigration = 1020; 259 | ProvisioningStyle = Automatic; 260 | }; 261 | 500F5BD6201CBD7A00B78822 = { 262 | CreatedOnToolsVersion = 9.2; 263 | LastSwiftMigration = 1020; 264 | ProvisioningStyle = Automatic; 265 | }; 266 | }; 267 | }; 268 | buildConfigurationList = 500F5BBE201CBCA700B78822 /* Build configuration list for PBXProject "AppGuideOverlay" */; 269 | compatibilityVersion = "Xcode 8.0"; 270 | developmentRegion = en; 271 | hasScannedForEncodings = 0; 272 | knownRegions = ( 273 | en, 274 | Base, 275 | ); 276 | mainGroup = 500F5BBA201CBCA700B78822; 277 | productRefGroup = 500F5BC5201CBCA700B78822 /* Products */; 278 | projectDirPath = ""; 279 | projectRoot = ""; 280 | targets = ( 281 | 500F5BC3201CBCA700B78822 /* AppGuideOverlay */, 282 | 500F5BD6201CBD7A00B78822 /* Example */, 283 | ); 284 | }; 285 | /* End PBXProject section */ 286 | 287 | /* Begin PBXResourcesBuildPhase section */ 288 | 500F5BC2201CBCA700B78822 /* Resources */ = { 289 | isa = PBXResourcesBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | 508876C520493F5500B0F748 /* finishTemplate.pdf in Resources */, 293 | 508876C720493F5900B0F748 /* nextTemplate.pdf in Resources */, 294 | 508876C920493F5E00B0F748 /* prevTemplate.pdf in Resources */, 295 | ); 296 | runOnlyForDeploymentPostprocessing = 0; 297 | }; 298 | 500F5BD5201CBD7A00B78822 /* Resources */ = { 299 | isa = PBXResourcesBuildPhase; 300 | buildActionMask = 2147483647; 301 | files = ( 302 | 500F5BDC201CBD7A00B78822 /* Assets.xcassets in Resources */, 303 | 500F5BDF201CBD7A00B78822 /* MainMenu.xib in Resources */, 304 | ); 305 | runOnlyForDeploymentPostprocessing = 0; 306 | }; 307 | /* End PBXResourcesBuildPhase section */ 308 | 309 | /* Begin PBXSourcesBuildPhase section */ 310 | 500F5BBF201CBCA700B78822 /* Sources */ = { 311 | isa = PBXSourcesBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | 500F5BE6201CBDA300B78822 /* OverlainWindow.swift in Sources */, 315 | 500F5BEC201CBF6F00B78822 /* AppGuideViewController.swift in Sources */, 316 | 507CF1962025DD0B0048F133 /* OverlayButtonLabels.swift in Sources */, 317 | 500F5BF2201DD1BE00B78822 /* OverlayLabelView.swift in Sources */, 318 | 500F5BE8201CBE2200B78822 /* AppGuide.swift in Sources */, 319 | 500F5BEA201CBF3400B78822 /* AppGuidePresenter.swift in Sources */, 320 | 50F54CC12025C0EF00C419E4 /* AppGuideOverlay.swift in Sources */, 321 | 500F2B2823D30492002B7BD1 /* OverlayButton.swift in Sources */, 322 | 500F5C01201DD3B500B78822 /* AnimationLoop.swift in Sources */, 323 | 500F5BF8201DD2A700B78822 /* NSTextField+Label.swift in Sources */, 324 | 507CF198202632D80048F133 /* DisplaysAppGuide.swift in Sources */, 325 | 500F5BF0201DD1A900B78822 /* OverlayLabelViewController.swift in Sources */, 326 | 500F5BEE201DD18C00B78822 /* OverlayView.swift in Sources */, 327 | 500F5BF4201DD1D600B78822 /* OverlayViewController.swift in Sources */, 328 | 500F5BFE201DD3B500B78822 /* ValueAnimationLoop.swift in Sources */, 329 | 500F5BFF201DD3B500B78822 /* SmoothAnimation.swift in Sources */, 330 | 500F5BF6201DD27A00B78822 /* NSView+constraints.swift in Sources */, 331 | ); 332 | runOnlyForDeploymentPostprocessing = 0; 333 | }; 334 | 500F5BD3201CBD7A00B78822 /* Sources */ = { 335 | isa = PBXSourcesBuildPhase; 336 | buildActionMask = 2147483647; 337 | files = ( 338 | 500F5C03201DD4D000B78822 /* NSView+constraints.swift in Sources */, 339 | 500F5BDA201CBD7A00B78822 /* AppDelegate.swift in Sources */, 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | }; 343 | /* End PBXSourcesBuildPhase section */ 344 | 345 | /* Begin PBXTargetDependency section */ 346 | 500F5C07201DD5D700B78822 /* PBXTargetDependency */ = { 347 | isa = PBXTargetDependency; 348 | target = 500F5BC3201CBCA700B78822 /* AppGuideOverlay */; 349 | targetProxy = 500F5C06201DD5D700B78822 /* PBXContainerItemProxy */; 350 | }; 351 | /* End PBXTargetDependency section */ 352 | 353 | /* Begin PBXVariantGroup section */ 354 | 500F5BDD201CBD7A00B78822 /* MainMenu.xib */ = { 355 | isa = PBXVariantGroup; 356 | children = ( 357 | 500F5BDE201CBD7A00B78822 /* Base */, 358 | ); 359 | name = MainMenu.xib; 360 | sourceTree = ""; 361 | }; 362 | /* End PBXVariantGroup section */ 363 | 364 | /* Begin XCBuildConfiguration section */ 365 | 500F5BCA201CBCA700B78822 /* Debug */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ALWAYS_SEARCH_USER_PATHS = NO; 369 | CLANG_ANALYZER_NONNULL = YES; 370 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 371 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 372 | CLANG_CXX_LIBRARY = "libc++"; 373 | CLANG_ENABLE_MODULES = YES; 374 | CLANG_ENABLE_OBJC_ARC = YES; 375 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 376 | CLANG_WARN_BOOL_CONVERSION = YES; 377 | CLANG_WARN_COMMA = YES; 378 | CLANG_WARN_CONSTANT_CONVERSION = YES; 379 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 380 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 381 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 382 | CLANG_WARN_EMPTY_BODY = YES; 383 | CLANG_WARN_ENUM_CONVERSION = YES; 384 | CLANG_WARN_INFINITE_RECURSION = YES; 385 | CLANG_WARN_INT_CONVERSION = YES; 386 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 387 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 388 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 389 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 390 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 391 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 392 | CLANG_WARN_STRICT_PROTOTYPES = YES; 393 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 394 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 395 | CLANG_WARN_UNREACHABLE_CODE = YES; 396 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 397 | CODE_SIGN_IDENTITY = "Mac Developer"; 398 | COPY_PHASE_STRIP = NO; 399 | CURRENT_PROJECT_VERSION = 1; 400 | DEAD_CODE_STRIPPING = YES; 401 | DEBUG_INFORMATION_FORMAT = dwarf; 402 | ENABLE_STRICT_OBJC_MSGSEND = YES; 403 | ENABLE_TESTABILITY = YES; 404 | GCC_C_LANGUAGE_STANDARD = gnu11; 405 | GCC_DYNAMIC_NO_PIC = NO; 406 | GCC_NO_COMMON_BLOCKS = YES; 407 | GCC_OPTIMIZATION_LEVEL = 0; 408 | GCC_PREPROCESSOR_DEFINITIONS = ( 409 | "DEBUG=1", 410 | "$(inherited)", 411 | ); 412 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 413 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 414 | GCC_WARN_UNDECLARED_SELECTOR = YES; 415 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 416 | GCC_WARN_UNUSED_FUNCTION = YES; 417 | GCC_WARN_UNUSED_VARIABLE = YES; 418 | MACOSX_DEPLOYMENT_TARGET = 10.13; 419 | MTL_ENABLE_DEBUG_INFO = YES; 420 | ONLY_ACTIVE_ARCH = YES; 421 | SDKROOT = macosx; 422 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 423 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 424 | VERSIONING_SYSTEM = "apple-generic"; 425 | VERSION_INFO_PREFIX = ""; 426 | }; 427 | name = Debug; 428 | }; 429 | 500F5BCB201CBCA700B78822 /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | ALWAYS_SEARCH_USER_PATHS = NO; 433 | CLANG_ANALYZER_NONNULL = YES; 434 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 435 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 436 | CLANG_CXX_LIBRARY = "libc++"; 437 | CLANG_ENABLE_MODULES = YES; 438 | CLANG_ENABLE_OBJC_ARC = YES; 439 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 440 | CLANG_WARN_BOOL_CONVERSION = YES; 441 | CLANG_WARN_COMMA = YES; 442 | CLANG_WARN_CONSTANT_CONVERSION = YES; 443 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 444 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 445 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 446 | CLANG_WARN_EMPTY_BODY = YES; 447 | CLANG_WARN_ENUM_CONVERSION = YES; 448 | CLANG_WARN_INFINITE_RECURSION = YES; 449 | CLANG_WARN_INT_CONVERSION = YES; 450 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 451 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 452 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 453 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 454 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 455 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 456 | CLANG_WARN_STRICT_PROTOTYPES = YES; 457 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 458 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 459 | CLANG_WARN_UNREACHABLE_CODE = YES; 460 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 461 | CODE_SIGN_IDENTITY = "Mac Developer"; 462 | COPY_PHASE_STRIP = NO; 463 | CURRENT_PROJECT_VERSION = 1; 464 | DEAD_CODE_STRIPPING = YES; 465 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 466 | ENABLE_NS_ASSERTIONS = NO; 467 | ENABLE_STRICT_OBJC_MSGSEND = YES; 468 | GCC_C_LANGUAGE_STANDARD = gnu11; 469 | GCC_NO_COMMON_BLOCKS = YES; 470 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 471 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 472 | GCC_WARN_UNDECLARED_SELECTOR = YES; 473 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 474 | GCC_WARN_UNUSED_FUNCTION = YES; 475 | GCC_WARN_UNUSED_VARIABLE = YES; 476 | MACOSX_DEPLOYMENT_TARGET = 10.13; 477 | MTL_ENABLE_DEBUG_INFO = NO; 478 | SDKROOT = macosx; 479 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 480 | VERSIONING_SYSTEM = "apple-generic"; 481 | VERSION_INFO_PREFIX = ""; 482 | }; 483 | name = Release; 484 | }; 485 | 500F5BCD201CBCA700B78822 /* Debug */ = { 486 | isa = XCBuildConfiguration; 487 | buildSettings = { 488 | CLANG_ENABLE_MODULES = YES; 489 | CODE_SIGN_IDENTITY = ""; 490 | CODE_SIGN_STYLE = Automatic; 491 | COMBINE_HIDPI_IMAGES = YES; 492 | DEAD_CODE_STRIPPING = YES; 493 | DEFINES_MODULE = YES; 494 | DEVELOPMENT_TEAM = ""; 495 | DYLIB_COMPATIBILITY_VERSION = 1; 496 | DYLIB_CURRENT_VERSION = 1; 497 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 498 | FRAMEWORK_VERSION = A; 499 | INFOPLIST_FILE = AppGuideOverlay/Info.plist; 500 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 501 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 502 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.AppGuideOverlay; 503 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 504 | SKIP_INSTALL = YES; 505 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 506 | SWIFT_VERSION = 5.0; 507 | }; 508 | name = Debug; 509 | }; 510 | 500F5BCE201CBCA700B78822 /* Release */ = { 511 | isa = XCBuildConfiguration; 512 | buildSettings = { 513 | CLANG_ENABLE_MODULES = YES; 514 | CODE_SIGN_IDENTITY = ""; 515 | CODE_SIGN_STYLE = Automatic; 516 | COMBINE_HIDPI_IMAGES = YES; 517 | DEAD_CODE_STRIPPING = YES; 518 | DEFINES_MODULE = YES; 519 | DEVELOPMENT_TEAM = ""; 520 | DYLIB_COMPATIBILITY_VERSION = 1; 521 | DYLIB_CURRENT_VERSION = 1; 522 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 523 | FRAMEWORK_VERSION = A; 524 | INFOPLIST_FILE = AppGuideOverlay/Info.plist; 525 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 526 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 527 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.AppGuideOverlay; 528 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 529 | SKIP_INSTALL = YES; 530 | SWIFT_VERSION = 5.0; 531 | }; 532 | name = Release; 533 | }; 534 | 500F5BE3201CBD7A00B78822 /* Debug */ = { 535 | isa = XCBuildConfiguration; 536 | buildSettings = { 537 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 538 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 539 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 540 | CODE_SIGN_IDENTITY = "-"; 541 | CODE_SIGN_STYLE = Automatic; 542 | COMBINE_HIDPI_IMAGES = YES; 543 | DEAD_CODE_STRIPPING = YES; 544 | DEVELOPMENT_TEAM = ""; 545 | INFOPLIST_FILE = Example/Info.plist; 546 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 547 | MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; 548 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.AppGuideOverlay.Example; 549 | PRODUCT_NAME = "$(TARGET_NAME)"; 550 | SWIFT_VERSION = 5.0; 551 | }; 552 | name = Debug; 553 | }; 554 | 500F5BE4201CBD7A00B78822 /* Release */ = { 555 | isa = XCBuildConfiguration; 556 | buildSettings = { 557 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 558 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 559 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 560 | CODE_SIGN_IDENTITY = "-"; 561 | CODE_SIGN_STYLE = Automatic; 562 | COMBINE_HIDPI_IMAGES = YES; 563 | DEAD_CODE_STRIPPING = YES; 564 | DEVELOPMENT_TEAM = ""; 565 | INFOPLIST_FILE = Example/Info.plist; 566 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 567 | MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; 568 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.AppGuideOverlay.Example; 569 | PRODUCT_NAME = "$(TARGET_NAME)"; 570 | SWIFT_VERSION = 5.0; 571 | }; 572 | name = Release; 573 | }; 574 | /* End XCBuildConfiguration section */ 575 | 576 | /* Begin XCConfigurationList section */ 577 | 500F5BBE201CBCA700B78822 /* Build configuration list for PBXProject "AppGuideOverlay" */ = { 578 | isa = XCConfigurationList; 579 | buildConfigurations = ( 580 | 500F5BCA201CBCA700B78822 /* Debug */, 581 | 500F5BCB201CBCA700B78822 /* Release */, 582 | ); 583 | defaultConfigurationIsVisible = 0; 584 | defaultConfigurationName = Release; 585 | }; 586 | 500F5BCC201CBCA700B78822 /* Build configuration list for PBXNativeTarget "AppGuideOverlay" */ = { 587 | isa = XCConfigurationList; 588 | buildConfigurations = ( 589 | 500F5BCD201CBCA700B78822 /* Debug */, 590 | 500F5BCE201CBCA700B78822 /* Release */, 591 | ); 592 | defaultConfigurationIsVisible = 0; 593 | defaultConfigurationName = Release; 594 | }; 595 | 500F5BE2201CBD7A00B78822 /* Build configuration list for PBXNativeTarget "Example" */ = { 596 | isa = XCConfigurationList; 597 | buildConfigurations = ( 598 | 500F5BE3201CBD7A00B78822 /* Debug */, 599 | 500F5BE4201CBD7A00B78822 /* Release */, 600 | ); 601 | defaultConfigurationIsVisible = 0; 602 | defaultConfigurationName = Release; 603 | }; 604 | /* End XCConfigurationList section */ 605 | }; 606 | rootObject = 500F5BBB201CBCA700B78822 /* Project object */; 607 | } 608 | -------------------------------------------------------------------------------- /AppGuideOverlay.xcodeproj/xcshareddata/xcschemes/AppGuideOverlay.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /AppGuideOverlay/AppGuide.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | public struct AppGuide { 6 | public let steps: [Step] 7 | 8 | public subscript (_ index: Int) -> Step { 9 | return steps[index] 10 | } 11 | 12 | public var count: Int { return steps.count } 13 | 14 | public init(steps: [Step]) { 15 | self.steps = steps 16 | } 17 | 18 | public struct Step { 19 | public let title: String 20 | public let detail: String 21 | public let position: Position 22 | public let cutoutView: NSView 23 | 24 | public init( 25 | title: String, 26 | detail: String, 27 | position: Position, 28 | cutoutView: NSView) { 29 | 30 | self.title = title 31 | self.detail = detail 32 | self.position = position 33 | self.cutoutView = cutoutView 34 | } 35 | 36 | public enum Position { 37 | case above, below, left, right 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /AppGuideOverlay/AppGuideOverlay.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppGuideOverlay.h 3 | // AppGuideOverlay 4 | // 5 | // Created by Christian Tietze on 27.01.18. 6 | // Copyright © 2018 Christian Tietze. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for AppGuideOverlay. 12 | FOUNDATION_EXPORT double AppGuideOverlayVersionNumber; 13 | 14 | //! Project version string for AppGuideOverlay. 15 | FOUNDATION_EXPORT const unsigned char AppGuideOverlayVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /AppGuideOverlay/AppGuideOverlay.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | public protocol AppGuideOverlayDelegate: AnyObject { 6 | /// Use to prepare your user interface to display all necessary elements. 7 | func appGuideWillAppear() 8 | 9 | /// Called when the guide was aborted, e.g. by hitting Esc. 10 | func appGuideDidCancel() 11 | 12 | /// Called when the guide was seen through to the end. 13 | func appGuideDidFinish() 14 | } 15 | 16 | public protocol HandlesOverlayEvents: AnyObject { 17 | func nextStep() 18 | func previousStep() 19 | func finish() 20 | func cancel() 21 | } 22 | 23 | /// Convenience service object for simple setup of overlays. 24 | /// 25 | /// Sets up an `AppGuidePresenter` and `AppGuideViewController`, forwarding 26 | /// control events to its `delegate`. 27 | /// 28 | /// Puts the app gudie overlay views into the view hierarchy on `start` and 29 | /// removes them when finished to clear the Auto Layout constraints. 30 | /// 31 | /// - React to events implementing `AppGuideOverlayDelegate`. 32 | /// - Change button labels using `OverlayButtonLabels`. 33 | open class AppGuideOverlay { 34 | 35 | open weak var delegate: AppGuideOverlayDelegate? 36 | 37 | public let appGuideViewController: AppGuideViewController 38 | 39 | open var appGuideView: NSView { return appGuideViewController.view } 40 | 41 | /// Setting for the containment view itself 42 | open var wantsLayer: Bool { 43 | get { return appGuideView.wantsLayer } 44 | set { appGuideView.wantsLayer = newValue } 45 | } 46 | 47 | open var overlayColor: NSColor { 48 | get { return appGuideViewController.overlayColor } 49 | set { appGuideViewController.overlayColor = newValue } 50 | } 51 | 52 | public let appGuidePresenter: AppGuidePresenter 53 | 54 | open var appGuide: AppGuide { return appGuidePresenter.appGuide } 55 | 56 | public let appGuideSuperview: NSView 57 | 58 | /// Indicates if invoking "next" via keyboard will automatically finish the 59 | /// sequence if it's at the end. Defaults to `false`. 60 | open var isFinishingAfterNext: Bool = false 61 | 62 | required public init(appGuide: AppGuide, appGuideSuperview: NSView) { 63 | 64 | self.appGuideViewController = AppGuideViewController() 65 | self.appGuidePresenter = AppGuidePresenter( 66 | appGuide: appGuide, 67 | view: appGuideViewController) 68 | self.appGuideSuperview = appGuideSuperview 69 | 70 | appGuideViewController.eventHandler = self 71 | } 72 | 73 | fileprivate func installAppGuideIntoSuperview() { 74 | 75 | appGuideSuperview.addSubview(self.appGuideView) 76 | appGuideView.constrainToSuperviewBounds() 77 | } 78 | 79 | fileprivate func removeAppGuideFromSuperview() { 80 | 81 | appGuideView.removeFromSuperview() 82 | } 83 | } 84 | 85 | extension AppGuideOverlay: HandlesOverlayEvents { 86 | 87 | public func start() { 88 | delegate?.appGuideWillAppear() 89 | 90 | installAppGuideIntoSuperview() 91 | 92 | appGuidePresenter.start() 93 | } 94 | 95 | public func nextStep() { 96 | if isFinishingAfterNext, 97 | !appGuidePresenter.hasNextStep { 98 | finish() 99 | return 100 | } 101 | 102 | appGuidePresenter.nextStep() 103 | } 104 | 105 | public func previousStep() { 106 | appGuidePresenter.previousStep() 107 | } 108 | 109 | public func cancel() { 110 | stop() 111 | delegate?.appGuideDidCancel() 112 | } 113 | 114 | public func finish() { 115 | stop() 116 | delegate?.appGuideDidFinish() 117 | } 118 | 119 | private func stop() { 120 | 121 | appGuidePresenter.cancel() 122 | removeAppGuideFromSuperview() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /AppGuideOverlay/AppGuidePresenter.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | /// Can be used as `HandlesOverlayEvents` controller out of the box to control 4 | /// the transition and view/hide its `view`. 5 | public class AppGuidePresenter: HandlesOverlayEvents { 6 | 7 | public let appGuide: AppGuide 8 | public let view: DisplaysAppGuide 9 | 10 | public init(appGuide: AppGuide, view: DisplaysAppGuide) { 11 | self.view = view 12 | self.appGuide = appGuide 13 | } 14 | 15 | private var stepIndex = 0 16 | public var hasNextStep: Bool { return stepIndex < (appGuide.count - 1) } 17 | public var hasPreviousStep: Bool { return stepIndex > 0 } 18 | 19 | public var currentStep: AppGuide.Step { return appGuide[stepIndex] } 20 | 21 | public func start() { 22 | stepIndex = 0 23 | displayStep() 24 | } 25 | 26 | 27 | public func nextStep() { 28 | guard hasNextStep else { return } 29 | stepIndex += 1 30 | displayStep() 31 | } 32 | 33 | public func previousStep() { 34 | guard hasPreviousStep else { return } 35 | stepIndex -= 1 36 | displayStep() 37 | } 38 | 39 | public func finish() { 40 | view.hide() 41 | stepIndex = 0 42 | } 43 | 44 | public func cancel() { 45 | view.hide() 46 | stepIndex = 0 47 | } 48 | 49 | public func displayStep() { 50 | precondition(appGuide.steps.indices.contains(stepIndex)) 51 | let viewModel = currentAppGuideStepViewModel() 52 | DispatchQueue.main.async { 53 | self.view.display(appGuideStep: viewModel) 54 | } 55 | } 56 | 57 | private func currentAppGuideStepViewModel() -> AppGuideStepViewModel { 58 | 59 | let base = currentStep 60 | 61 | let isFirstStep = !hasPreviousStep 62 | let isLastStep = !hasNextStep 63 | 64 | return AppGuideStepViewModel( 65 | title: base.title, 66 | detail: base.detail, 67 | isFirstStep: isFirstStep, 68 | isLastStep: isLastStep, 69 | position: base.position, 70 | cutoutView: base.cutoutView) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /AppGuideOverlay/AppGuideViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | open class AppGuideViewController: NSViewController, DisplaysAppGuide { 6 | 7 | open weak var eventHandler: HandlesOverlayEvents? { 8 | get { return overlayViewController.eventHandler } 9 | set { overlayViewController.eventHandler = newValue } 10 | } 11 | 12 | open lazy var overlayViewController: OverlayViewController = OverlayViewController() 13 | 14 | open var overlayColor: NSColor { 15 | get { return overlayViewController.overlayColor } 16 | set { overlayViewController.overlayColor = newValue } 17 | } 18 | 19 | open lazy var overlayLabelViewController: OverlayLabelViewController = OverlayLabelViewController() 20 | 21 | /// Spacing between the cutout view and its overlay labels. 22 | open var overlayLabelSpacing: CGFloat = 12 23 | 24 | open override func loadView() { 25 | 26 | self.view = NSView() 27 | } 28 | 29 | open override func viewDidLoad() { 30 | 31 | self.view.addSubview(overlayViewController.overlayView) 32 | overlayViewController.overlayView.constrainToSuperviewBounds() 33 | 34 | overlayViewController.overlayView.addSubview(overlayLabelViewController.overlayLabelView) 35 | addLabelConstraints(overlayLabelView: overlayLabelViewController.overlayLabelView) 36 | } 37 | 38 | /// Ensure the constrainingView always fits the overlayLabelView. 39 | private func addLabelConstraints(overlayLabelView: OverlayLabelView) { 40 | 41 | let views: [String : Any] = ["labels" : overlayLabelView] 42 | let constrainingView = self.view 43 | constrainingView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(>=20)-[labels]-(>=20)-|", options: [], metrics: nil, views: views)) 44 | constrainingView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-(>=20)-[labels]-(>=20)-|", options: [], metrics: nil, views: views)) 45 | } 46 | 47 | open func display(appGuideStep: AppGuideStepViewModel) { 48 | 49 | overlayViewController.display( 50 | appGuideStep: appGuideStep) 51 | overlayLabelViewController.display( 52 | appGuideStep: appGuideStep, 53 | spacing: overlayLabelSpacing) 54 | 55 | // `layoutSubtreeIfNeeded()` didn't force the frame to change before the cutout would be drawn at the wrong coordinates, but `layout()` does. 56 | overlayViewController.overlayView.layout() 57 | 58 | ensureOverlayPartIsFirstResponder() 59 | } 60 | 61 | /// Retain the first responder when it's a `OverlayPart` or change it to the "next" button. 62 | private func ensureOverlayPartIsFirstResponder() { 63 | 64 | guard let window = self.view.window else { return } 65 | 66 | if !(window.firstResponder is OverlayPart) { 67 | window.makeFirstResponder(overlayLabelViewController.overlayLabelView.nextStepButton) 68 | } 69 | } 70 | 71 | open func hide() { 72 | 73 | overlayViewController.hide() 74 | overlayLabelViewController.hide() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /AppGuideOverlay/DisplaysAppGuide.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import class AppKit.NSView 4 | 5 | public protocol DisplaysAppGuide { 6 | func display(appGuideStep: AppGuideStepViewModel) 7 | func hide() 8 | } 9 | 10 | public struct AppGuideStepViewModel { 11 | 12 | public let title: String 13 | public let detail: String 14 | 15 | public let isFirstStep: Bool 16 | public let isLastStep: Bool 17 | 18 | public let position: AppGuide.Step.Position 19 | public let cutoutView: NSView 20 | 21 | public init( 22 | title: String, 23 | detail: String, 24 | isFirstStep: Bool, 25 | isLastStep: Bool, 26 | position: AppGuide.Step.Position, 27 | cutoutView: NSView) { 28 | 29 | self.title = title 30 | self.detail = detail 31 | 32 | self.isFirstStep = isFirstStep 33 | self.isLastStep = isLastStep 34 | 35 | self.position = position 36 | self.cutoutView = cutoutView 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AppGuideOverlay/Images/finishTemplate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CleanCocoa/AppGuideOverlay/69efdfae778fec747c5731c4292f8d7badcdbcff/AppGuideOverlay/Images/finishTemplate.pdf -------------------------------------------------------------------------------- /AppGuideOverlay/Images/nextTemplate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CleanCocoa/AppGuideOverlay/69efdfae778fec747c5731c4292f8d7badcdbcff/AppGuideOverlay/Images/nextTemplate.pdf -------------------------------------------------------------------------------- /AppGuideOverlay/Images/prevTemplate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CleanCocoa/AppGuideOverlay/69efdfae778fec747c5731c4292f8d7badcdbcff/AppGuideOverlay/Images/prevTemplate.pdf -------------------------------------------------------------------------------- /AppGuideOverlay/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2018 Christian Tietze. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /AppGuideOverlay/LoopingAnimation/AnimationLoop.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | //  3 | 4 | import AppKit 5 | 6 | internal class AnimationLoop: NSObject, NSAnimationDelegate { 7 | 8 | internal let configuration: LoopConfiguration 9 | 10 | private var runningAnimation: DirectedAnimation! 11 | internal var currentOperation: Operation { return runningAnimation.operation } 12 | internal var isAnimating: Bool { return runningAnimation.isAnimating } 13 | 14 | /// Value betweem 0.0 and 1.0 15 | internal typealias Progress = CGFloat 16 | internal var progressHandler: ((Progress, Operation) -> Void)? 17 | 18 | /// Sets up an animation loop. 19 | /// 20 | /// - parameter configuration: Configuration of the looping animation parts. 21 | /// - parameter initialOperation: Which animation loop operation to start with. Defaults to `.increment`. 22 | internal required init( 23 | configuration: LoopConfiguration, 24 | startWith initialOperation: Operation = .increment) { 25 | 26 | self.configuration = configuration 27 | 28 | super.init() 29 | 30 | self.runningAnimation = createAnimation(operation: initialOperation) 31 | } 32 | 33 | /// Sets up a loop with a `LoopConfiguration` of the parameters from this initializer. 34 | /// 35 | /// - parameter increaseDuration: Time the increase animations will take. 36 | /// - parameter increaseCurve: Animation curve of increase animations. 37 | /// - parameter decreaseDuration: Time the decreaste animations will take. 38 | /// - parameter decreaseCurve: Animation curve of decrease animations. 39 | /// - parameter initialOperation: Which animation loop operation to start with. Defaults to `.increment`. 40 | internal convenience init( 41 | increaseDuration: TimeInterval, 42 | increaseCurve: NSAnimation.Curve, 43 | decreaseDuration: TimeInterval, 44 | decreaseCurve: NSAnimation.Curve, 45 | startWith initialOperation: Operation = .increment) { 46 | 47 | self.init( 48 | configuration: LoopConfiguration( 49 | increase: .init(duration: increaseDuration, animationCurve: increaseCurve), 50 | decrease: .init(duration: decreaseDuration, animationCurve: decreaseCurve)), 51 | startWith: initialOperation) 52 | } 53 | 54 | /// Sets up a loop of 2 animations with increase and decrease both 55 | /// configured the same way. 56 | /// 57 | /// - parameter duration: Duration of both the increase and decrease animation. 58 | /// - parameter animationCurve: Animation curve of both the increase and decrease animation. 59 | internal convenience init(duration: TimeInterval, animationCurve: NSAnimation.Curve) { 60 | self.init(increaseDuration: duration, increaseCurve: animationCurve, 61 | decreaseDuration: duration, decreaseCurve: animationCurve) 62 | } 63 | 64 | /// Sets up a loop of 2 animations with a default duration of 1 second 65 | /// and `.easeInOut` animation curve. 66 | internal convenience override init() { 67 | self.init(duration: 1, animationCurve: .easeInOut) 68 | } 69 | 70 | private func createAnimation(operation: Operation) -> DirectedAnimation { 71 | 72 | let animation = configuration.step(operation: operation).smoothAnimation() 73 | animation.animationBlockingMode = .nonblocking 74 | animation.delegate = self 75 | animation.progressHandler = { [weak self] in self?.animationDidProgress($0) } 76 | return DirectedAnimation( 77 | animation: animation, 78 | operation: operation) 79 | } 80 | 81 | private func animationDidProgress(_ progress: NSAnimation.Progress) { 82 | progressHandler?(CGFloat(progress), currentOperation) 83 | } 84 | 85 | internal func animationDidEnd(_ animation: NSAnimation) { 86 | guard animation === self.runningAnimation.animation else { return } 87 | startNextAnimation() 88 | } 89 | 90 | private func startNextAnimation() { 91 | let nextOperation = !self.currentOperation 92 | let nextAnimation = createAnimation(operation: nextOperation) 93 | self.runningAnimation = nextAnimation 94 | nextAnimation.start() 95 | } 96 | 97 | internal func start() { 98 | 99 | guard !isAnimating else { preconditionFailure("Cannot start while running") } 100 | 101 | runningAnimation.start() 102 | } 103 | 104 | internal func reset() { 105 | 106 | self.runningAnimation.cancel() 107 | self.runningAnimation = createAnimation(operation: .increment) 108 | } 109 | 110 | internal struct DirectedAnimation { 111 | 112 | internal let animation: SmoothAnimation 113 | internal var isAnimating: Bool { return animation.isAnimating } 114 | 115 | internal let operation: Operation 116 | 117 | internal init(animation: SmoothAnimation, operation: Operation) { 118 | self.animation = animation 119 | self.operation = operation 120 | } 121 | 122 | internal func start() { 123 | animation.start() 124 | } 125 | 126 | internal func cancel() { 127 | // Do not forward the last frame event that `stop()` will emit. 128 | animation.progressHandler = { _ in } 129 | animation.stop() 130 | } 131 | } 132 | 133 | internal enum Operation { 134 | case increment 135 | case decrement 136 | 137 | internal static prefix func !(_ operation: Operation) -> Operation { 138 | if operation ~= .increment { return .decrement } 139 | return .increment 140 | } 141 | } 142 | 143 | internal struct LoopConfiguration { 144 | internal let increase: AnimationStep 145 | internal let decrease: AnimationStep 146 | 147 | internal init(increase: AnimationStep, decrease: AnimationStep) { 148 | self.increase = increase 149 | self.decrease = decrease 150 | } 151 | 152 | internal func step(operation: Operation) -> AnimationStep { 153 | switch operation { 154 | case .increment: return increase 155 | case .decrement: return decrease 156 | } 157 | } 158 | 159 | internal struct AnimationStep { 160 | internal let duration: TimeInterval 161 | internal let animationCurve: NSAnimation.Curve 162 | 163 | internal init(duration: TimeInterval, animationCurve: NSAnimation.Curve) { 164 | self.duration = duration 165 | self.animationCurve = animationCurve 166 | } 167 | 168 | internal func smoothAnimation() -> SmoothAnimation { 169 | return SmoothAnimation(duration: duration, animationCurve: animationCurve) 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /AppGuideOverlay/LoopingAnimation/SmoothAnimation.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | //  3 | 4 | import AppKit 5 | 6 | /// `NSAnimation` that reports smooth, un-marked animation progress directly through `progressHandler`. 7 | internal class SmoothAnimation: NSAnimation { 8 | internal var progressHandler: ((NSAnimation.Progress) -> Void)? 9 | internal override var currentProgress: NSAnimation.Progress { 10 | didSet { 11 | progressHandler?(currentProgress) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AppGuideOverlay/LoopingAnimation/ValueAnimationLoop.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | //  3 | 4 | import AppKit 5 | 6 | /// Animation loop that translates animation progress to `value`. 7 | /// 8 | /// It animates incrementing from 0...`value` and back again from `value`...0. 9 | internal class ValueAnimationLoop { 10 | 11 | internal let loop: AnimationLoop 12 | internal let value: CGFloat 13 | 14 | internal var progressHandler: ((CGFloat) -> Void)? 15 | 16 | internal init(value: CGFloat, loop: AnimationLoop) { 17 | self.value = value 18 | self.loop = loop 19 | } 20 | 21 | /// - parameter value: The maximum value to loop to, and from which to loop back again to 0. 22 | /// - parameter increaseDuration: Time the increase animations will take. 23 | /// - parameter increaseCurve: Animation curve of increase animations. 24 | /// - parameter decreaseDuration: Time the decreaste animations will take. 25 | /// - parameter decreaseCurve: Animation curve of decrease animations. 26 | /// - parameter initialOperation: Which animation loop operation to start with. Defaults to `.increment`. 27 | internal convenience init( 28 | value: CGFloat, 29 | increaseDuration: TimeInterval = 1.0, 30 | increaseCurve: NSAnimation.Curve = .easeInOut, 31 | decreaseDuration: TimeInterval = 1.0, 32 | decreaseCurve: NSAnimation.Curve = .easeInOut, 33 | startWith initialOperation: AnimationLoop.Operation = .increment) { 34 | 35 | self.init( 36 | value: value, 37 | loop: AnimationLoop( 38 | increaseDuration: increaseDuration, 39 | increaseCurve: increaseCurve, 40 | decreaseDuration: decreaseDuration, 41 | decreaseCurve: decreaseCurve, 42 | startWith: initialOperation)) 43 | 44 | self.loop.progressHandler = { [weak self] in self?.loopDidProgress(progress: $0, operation: $1) } 45 | } 46 | 47 | private func loopDidProgress(progress: AnimationLoop.Progress, operation: AnimationLoop.Operation) { 48 | 49 | guard let progressHandler = progressHandler else { return } 50 | 51 | let progressedValue: CGFloat = { 52 | switch operation { 53 | case .increment: return progress * value 54 | case .decrement: return value - (progress * value) 55 | } 56 | }() 57 | 58 | progressHandler(progressedValue) 59 | } 60 | 61 | internal var isAnimating: Bool { return loop.isAnimating } 62 | 63 | internal func start() { 64 | loop.start() 65 | } 66 | 67 | internal func reset() { 68 | loop.reset() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /AppGuideOverlay/NSTextField+Label.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | extension NSTextField { 6 | 7 | private func fittingSystemFont() -> NSFont { 8 | return NSFont.systemFont(ofSize: NSFont.systemFontSize(for: self.controlSize)) 9 | } 10 | 11 | /// Return an `NSTextField` configured exactly like one created by dragging a “Label” into a storyboard. 12 | class func newLabel( 13 | title: String = "", 14 | controlSize: NSControl.ControlSize = .regular) -> NSTextField { 15 | 16 | let label = NSTextField() 17 | label.isEditable = false 18 | label.isSelectable = false 19 | label.textColor = .labelColor 20 | label.backgroundColor = .controlColor 21 | label.drawsBackground = false 22 | label.isBezeled = false 23 | label.alignment = .natural 24 | label.controlSize = controlSize 25 | label.font = label.fittingSystemFont() 26 | label.lineBreakMode = .byClipping 27 | label.cell?.isScrollable = true 28 | label.cell?.wraps = false 29 | label.stringValue = title 30 | return label 31 | } 32 | 33 | /// Return an `NSTextField` configured exactly like one created by dragging a “Wrapping Label” into a storyboard. 34 | class func newWrappingLabel( 35 | title: String = "", 36 | controlSize: NSControl.ControlSize = .regular) -> NSTextField { 37 | 38 | let label = newLabel(title: title, controlSize: controlSize) 39 | label.lineBreakMode = .byWordWrapping 40 | label.cell?.isScrollable = false 41 | label.cell?.wraps = true 42 | return label 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /AppGuideOverlay/NSView+constraints.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | internal extension NSView { 6 | func constrainToSuperviewBounds() { 7 | 8 | guard let superview = self.superview 9 | else { preconditionFailure("superview has to be set first") } 10 | 11 | self.translatesAutoresizingMaskIntoConstraints = false 12 | superview.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["subview": self])) 13 | superview.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["subview": self])) 14 | } 15 | 16 | func addConstraints(visualFormats: [String], views: [String : Any]) { 17 | 18 | let constraints = visualFormats 19 | .map { NSLayoutConstraint.constraints(withVisualFormat: $0, options: [], metrics: nil, views: views) } 20 | .joined() 21 | .asArray() 22 | self.addConstraints(constraints) 23 | } 24 | } 25 | 26 | internal extension Sequence { 27 | func asArray() -> [Element] { 28 | return Array(self) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /AppGuideOverlay/OverlainWindow.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | /// Marker of views which can become `firstResponder` in `OverlainWindow`. 6 | public protocol OverlayPart { } 7 | 8 | /// Prevents anything that is not an `OverlayPart` to become first responder. 9 | /// This is used to ensure that the user cannot focus views below the overlay. 10 | open class OverlainWindow: NSWindow { 11 | 12 | open var isDisplayingOverlay = false 13 | 14 | @discardableResult 15 | open override func makeFirstResponder(_ responder: NSResponder?) -> Bool { 16 | if isDisplayingOverlay { 17 | guard responder is OverlayPart else { return false } 18 | } 19 | return super.makeFirstResponder(responder) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /AppGuideOverlay/OverlayButton.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | open class OverlayButton: DarkButton, OverlayPart { 6 | 7 | public enum Action { 8 | case previous, next, finish 9 | 10 | public var imageName: NSImage.Name { 11 | switch self { 12 | case .previous: return "prevTemplate.pdf" 13 | case .next: return "nextTemplate.pdf" 14 | case .finish: return "finishTemplate.pdf" 15 | } 16 | } 17 | 18 | public var image: NSImage? { 19 | return Bundle(for: OverlayButton.self).image(forResource: imageName) 20 | } 21 | 22 | public var imagePosition: NSControl.ImagePosition { 23 | switch self { 24 | case .previous: return .imageLeft 25 | case .next, .finish: return .imageRight 26 | } 27 | } 28 | 29 | public var title: String { 30 | switch self { 31 | case .previous: return OverlayButtonLabels.previous 32 | case .next: return OverlayButtonLabels.next 33 | case .finish: return OverlayButtonLabels.finish 34 | } 35 | } 36 | 37 | public func configure(button: OverlayButton) { 38 | button.title = self.title 39 | button.image = self.image 40 | button.imagePosition = self.imagePosition 41 | button.imageScaling = .scaleProportionallyDown 42 | button.sizeToFit() 43 | } 44 | } 45 | 46 | internal func changeNextImage(isLastStep: Bool) { 47 | let action: Action = isLastStep ? .finish : .next 48 | self.image = action.image 49 | } 50 | } 51 | 52 | // MARK: - Fixes for unlegible text and images in dark buttons 53 | 54 | /// To fix the dark button appearance in light mode, the image and title colors are 55 | /// customized. Automatic updates happen when you change any of these: 56 | /// 57 | /// - `title`: changes the `attributedTitle` with the correct color 58 | /// - `templateImage`: updates `image` with a tint; 59 | /// - `image`: as a fallback, when the `image.isTemplate` is `true`, overrides `templateImage` and installs a non-tinted copy 60 | /// - `isEnabled`: updates `image` and `title` with the expected tints 61 | open class DarkButton: NSButton { 62 | 63 | override open var title: String { 64 | didSet { 65 | updateTitleColor() 66 | } 67 | } 68 | 69 | private func updateTitleColor() { 70 | var attributes = self.attributedTitle.attributes( 71 | at: 0, 72 | longestEffectiveRange: nil, 73 | in: NSRange(location: 0, length: self.attributedTitle.length)) 74 | attributes[.foregroundColor] = isEnabled ? NSColor.white : NSColor.black 75 | 76 | self.attributedTitle = NSAttributedString( 77 | string: self.title, 78 | attributes: attributes) 79 | } 80 | 81 | open override var image: NSImage? { 82 | didSet { 83 | guard let image = self.image else { return } 84 | guard image.isTemplate else { return } 85 | self.templateImage = image 86 | } 87 | } 88 | 89 | var templateImage: NSImage? { 90 | didSet { 91 | updateImageFromTemplate() 92 | } 93 | } 94 | 95 | @available(OSX 10.14, *) 96 | open override func viewDidChangeEffectiveAppearance() { 97 | super.viewDidChangeEffectiveAppearance() 98 | updateTitleColor() 99 | } 100 | 101 | override open var isEnabled: Bool { 102 | didSet { 103 | updateTitleColor() 104 | updateImageFromTemplate() 105 | } 106 | } 107 | 108 | private func updateImageFromTemplate() { 109 | guard let templateImage = self.templateImage else { return } 110 | self.image = templateImage.tintedNonTemplate(color: self.isEnabled ? .white : .lightGray) 111 | } 112 | 113 | } 114 | 115 | extension NSImage { 116 | fileprivate func tintedNonTemplate(color: NSColor) -> NSImage { 117 | let image = self.copy() as! NSImage 118 | image.lockFocus() 119 | 120 | color.set() 121 | 122 | let imageRect = NSRect(origin: NSZeroPoint, size: image.size) 123 | imageRect.fill(using: .sourceAtop) 124 | 125 | image.unlockFocus() 126 | 127 | // To distinguish the original vector template from the tinted variant, make it not a template 128 | image.isTemplate = false 129 | 130 | return image 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /AppGuideOverlay/OverlayButtonLabels.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | /// Default button labels, loaded as localized strings. Can be overridden. 4 | public struct OverlayButtonLabels { 5 | /// Name of the localization table file. 6 | public static let tableName = "AppGuideOverlay" 7 | 8 | /// `AppGuideOverlay.Previous` in `AppGuideOverlay.strings` 9 | static var previous = NSLocalizedString("AppGuideOverlay.Previous", tableName: OverlayButtonLabels.tableName, bundle: Bundle.main, value: "Previous", comment: "") 10 | 11 | /// `AppGuideOverlay.Next` in `AppGuideOverlay.strings` 12 | static var next = NSLocalizedString("AppGuideOverlay.Next", tableName: OverlayButtonLabels.tableName, bundle: Bundle.main, value: "Continue", comment: "") 13 | 14 | /// `AppGuideOverlay.Finish` in `AppGuideOverlay.strings` 15 | static var finish = NSLocalizedString("AppGuideOverlay.Finish", tableName: OverlayButtonLabels.tableName, bundle: Bundle.main, value: "Complete Guide", comment: "") 16 | } 17 | -------------------------------------------------------------------------------- /AppGuideOverlay/OverlayLabelView.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | open class OverlayLabelView: NSView { 6 | 7 | public let titleLabel: NSTextField 8 | public let detailLabel: NSTextField 9 | 10 | public let previousStepButton: OverlayButton 11 | public let nextStepButton: OverlayButton 12 | 13 | public override init(frame frameRect: NSRect) { 14 | 15 | self.titleLabel = OverlayLabelView.newTitleLabel() 16 | self.detailLabel = OverlayLabelView.newDetailLabel() 17 | self.previousStepButton = OverlayLabelView.newButton(direction: .previous) 18 | self.nextStepButton = OverlayLabelView.newButton(direction: .next) 19 | 20 | super.init(frame: frameRect) 21 | 22 | layoutLabels() 23 | setupKeyViewLoop() 24 | } 25 | 26 | public required init?(coder decoder: NSCoder) { 27 | 28 | self.titleLabel = OverlayLabelView.newTitleLabel() 29 | self.detailLabel = OverlayLabelView.newDetailLabel() 30 | self.previousStepButton = OverlayLabelView.newButton(direction: .previous) 31 | self.nextStepButton = OverlayLabelView.newButton(direction: .next) 32 | 33 | super.init(coder: decoder) 34 | 35 | layoutLabels() 36 | setupKeyViewLoop() 37 | } 38 | 39 | private static func newTitleLabel() -> NSTextField { 40 | 41 | let titleLabel = NSTextField.newLabel() 42 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 43 | titleLabel.font = NSFont.systemFont(ofSize: 24, weight: .light) 44 | titleLabel.textColor = .white 45 | titleLabel.identifier = .init(rawValue: "OverlayTitleLabel") 46 | return titleLabel 47 | } 48 | 49 | private static func newDetailLabel() -> NSTextField { 50 | 51 | let detailLabel = NSTextField.newWrappingLabel() 52 | detailLabel.translatesAutoresizingMaskIntoConstraints = false 53 | detailLabel.font = NSFont.systemFont(ofSize: 14, weight: .regular) 54 | detailLabel.textColor = .white 55 | detailLabel.identifier = .init(rawValue: "OverlayDetailLabel") 56 | return detailLabel 57 | } 58 | 59 | private static func newButton(direction: OverlayButton.Action) -> OverlayButton { 60 | 61 | let button = OverlayButton() 62 | button.translatesAutoresizingMaskIntoConstraints = false 63 | button.bezelStyle = .recessed 64 | button.setButtonType(.momentaryLight) 65 | 66 | direction.configure(button: button) 67 | 68 | return button 69 | } 70 | 71 | open var target: Any? 72 | open var previousAction: Selector? 73 | open var nextAction: Selector? 74 | open var finishAction: Selector? 75 | 76 | private func layoutLabels() { 77 | 78 | addSubview(titleLabel) 79 | addSubview(detailLabel) 80 | addSubview(previousStepButton) 81 | addSubview(nextStepButton) 82 | 83 | self.addConstraints( 84 | visualFormats: [ 85 | "H:|[title]|", 86 | "H:|[detail]|", 87 | "H:|[prev]-(12)-[next]-(>=0)-|", 88 | 89 | "V:|[title]-(8)-[detail]-(12)-[prev]|" 90 | ], 91 | views: [ 92 | "title" : titleLabel, 93 | "detail" : detailLabel, 94 | "prev" : previousStepButton, 95 | "next" : nextStepButton 96 | ]) 97 | self.addConstraints([NSLayoutConstraint(item: nextStepButton, attribute: .firstBaseline, relatedBy: .equal, toItem: previousStepButton, attribute: .firstBaseline, multiplier: 1, constant: 0)]) 98 | } 99 | 100 | private func setupKeyViewLoop() { 101 | 102 | previousStepButton.nextKeyView = nextStepButton 103 | nextStepButton.nextKeyView = previousStepButton 104 | } 105 | 106 | open func changeText(title: String, detail: String) { 107 | 108 | titleLabel.stringValue = title 109 | titleLabel.sizeToFit() 110 | 111 | detailLabel.stringValue = detail 112 | detailLabel.sizeToFit() 113 | 114 | layoutSubtreeIfNeeded() 115 | } 116 | 117 | open func changeButtons(isFirstStep: Bool, isLastStep: Bool) { 118 | 119 | previousStepButton.isEnabled = !isFirstStep 120 | previousStepButton.target = nil 121 | previousStepButton.action = previousAction 122 | 123 | nextStepButton.title = isLastStep 124 | ? OverlayButtonLabels.finish 125 | : OverlayButtonLabels.next 126 | nextStepButton.target = nil 127 | nextStepButton.action = isLastStep 128 | ? self.finishAction 129 | : self.nextAction 130 | nextStepButton.changeNextImage(isLastStep: isLastStep) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /AppGuideOverlay/OverlayLabelViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | open class OverlayLabelViewController: NSViewController { 6 | 7 | open var overlayLabelView: OverlayLabelView { 8 | get { return self.view as! OverlayLabelView } 9 | set { self.view = newValue } 10 | } 11 | 12 | private var constrainingView: NSView? { return self.view.window?.contentView } 13 | 14 | private var activeConstraints: [NSLayoutConstraint] = [] 15 | 16 | open override func loadView() { 17 | 18 | let overlayLabelView = OverlayLabelView() 19 | overlayLabelView.translatesAutoresizingMaskIntoConstraints = false 20 | 21 | // Pass next/prev actions up the responder chain 22 | overlayLabelView.target = nil 23 | overlayLabelView.nextAction = #selector(OverlayViewController.nextStep(_:)) 24 | overlayLabelView.previousAction = #selector(OverlayViewController.previousStep(_:)) 25 | overlayLabelView.finishAction = #selector(OverlayViewController.finishGuide(_:)) 26 | 27 | self.view = overlayLabelView 28 | } 29 | 30 | open private(set) var appGuideStepViewModel: AppGuideStepViewModel? 31 | 32 | open func display(appGuideStep: AppGuideStepViewModel, spacing: CGFloat) { 33 | 34 | self.overlayLabelView.isHidden = false 35 | self.appGuideStepViewModel = appGuideStep 36 | 37 | // Update texts first to calculate the view size properly 38 | updateLabels(title: appGuideStep.title, 39 | detail: appGuideStep.detail) 40 | updateButtons(isFirstStep: appGuideStep.isFirstStep, 41 | isLastStep: appGuideStep.isLastStep) 42 | replaceActiveConstraints(referenceView: appGuideStep.cutoutView, 43 | position: appGuideStep.position, 44 | spacing: spacing) 45 | } 46 | 47 | private func updateLabels(title: String, detail: String) { 48 | 49 | overlayLabelView.changeText(title: title, detail: detail) 50 | } 51 | 52 | private func updateButtons(isFirstStep: Bool, isLastStep: Bool) { 53 | 54 | overlayLabelView.changeButtons(isFirstStep: isFirstStep, isLastStep: isLastStep) 55 | } 56 | 57 | private func replaceActiveConstraints( 58 | referenceView: NSView, 59 | position: AppGuide.Step.Position, 60 | spacing: CGFloat) { 61 | 62 | guard let constrainingView = constrainingView else { preconditionFailure("View needs to be embedded in a window's view hierarchy before displaying a value.") } 63 | 64 | let newConstraints: [NSLayoutConstraint] = { 65 | switch position { 66 | case .below: return [ 67 | NSLayoutConstraint(item: overlayLabelView, attribute: .leading, relatedBy: .equal, toItem: referenceView, attribute: .leading, multiplier: 1, constant: 0).prioritized(.windowSizeStayPut), 68 | NSLayoutConstraint(item: overlayLabelView, attribute: .top, relatedBy: .equal, toItem: referenceView, attribute: .bottom, multiplier: 1, constant: spacing) 69 | ] 70 | 71 | case .above: return [ 72 | NSLayoutConstraint(item: overlayLabelView, attribute: .leading, relatedBy: .equal, toItem: referenceView, attribute: .leading, multiplier: 1, constant: 0).prioritized(.windowSizeStayPut), 73 | NSLayoutConstraint(item: referenceView, attribute: .top, relatedBy: .equal, toItem: overlayLabelView, attribute: .bottom, multiplier: 1, constant: spacing) 74 | ] 75 | 76 | case .left: return [ 77 | NSLayoutConstraint(item: referenceView, attribute: .leading, relatedBy: .equal, toItem: overlayLabelView, attribute: .trailing, multiplier: 1, constant: spacing), 78 | NSLayoutConstraint(item: overlayLabelView, attribute: .top, relatedBy: .equal, toItem: referenceView, attribute: .top, multiplier: 1, constant: 0).prioritized(.windowSizeStayPut) 79 | ] 80 | 81 | case .right: return [ 82 | NSLayoutConstraint(item: overlayLabelView, attribute: .leading, relatedBy: .equal, toItem: referenceView, attribute: .trailing, multiplier: 1, constant: spacing), 83 | NSLayoutConstraint(item: overlayLabelView, attribute: .top, relatedBy: .equal, toItem: referenceView, attribute: .top, multiplier: 1, constant: 0).prioritized(.windowSizeStayPut) 84 | ] 85 | } 86 | }() 87 | 88 | constrainingView.removeConstraints(activeConstraints) 89 | constrainingView.addConstraints(newConstraints) 90 | self.activeConstraints = newConstraints 91 | } 92 | 93 | open func hide() { 94 | self.overlayLabelView.isHidden = true 95 | } 96 | } 97 | 98 | extension NSLayoutConstraint { 99 | func prioritized(_ priority: NSLayoutConstraint.Priority) -> NSLayoutConstraint { 100 | self.priority = priority 101 | return self 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /AppGuideOverlay/OverlayView.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | open class OverlayView: NSView, OverlayPart { 6 | 7 | open override var canBecomeKeyView: Bool { return true } 8 | open override var isFlipped: Bool { return true } 9 | open override var frame: NSRect { 10 | didSet { 11 | updateCutout() 12 | } 13 | } 14 | 15 | open override var isHidden: Bool { 16 | didSet { 17 | if isHidden { cutoutBreathingLoop.reset() } 18 | else { animateCutoutBreathing() } 19 | 20 | resetCursorOverride() 21 | } 22 | } 23 | 24 | open var overlayColor = NSColor(white: 0, alpha: 0.4) 25 | private(set) var appGuideStepViewModel: AppGuideStepViewModel? { 26 | didSet { 27 | self.isHidden = (appGuideStepViewModel == nil) 28 | self.updateCutout() 29 | } 30 | } 31 | 32 | open func display(appGuideStep: AppGuideStepViewModel) { 33 | 34 | self.appGuideStepViewModel = appGuideStep 35 | } 36 | 37 | open func hideOverlayStep() { 38 | 39 | self.appGuideStepViewModel = nil 40 | } 41 | 42 | private func animateCutoutBreathing() { 43 | guard appGuideStepViewModel != nil else { return } 44 | guard !cutoutBreathingLoop.isAnimating else { return } 45 | cutoutBreathingLoop.start() 46 | } 47 | 48 | private lazy var cutoutBreathingLoop: ValueAnimationLoop = { 49 | let animationLoop = ValueAnimationLoop( 50 | value: 4, 51 | increaseDuration: 1.5, 52 | increaseCurve: .easeIn, 53 | decreaseDuration: 2, 54 | decreaseCurve: .easeOut) 55 | animationLoop.progressHandler = { [weak self] in self?.cutoutPadding = $0 } 56 | return animationLoop 57 | }() 58 | 59 | private var cutoutPadding: CGFloat = 0 { 60 | didSet { 61 | updateCutout() 62 | } 63 | } 64 | 65 | private var cutoutPath: NSBezierPath? 66 | 67 | private func updateCutout() { 68 | 69 | defer { self.needsDisplay = true } 70 | 71 | guard let cutoutView = appGuideStepViewModel?.cutoutView else { 72 | cutoutPath = nil 73 | return 74 | } 75 | 76 | let referenceViewRect = cutoutView.superview!.convert(cutoutView.frame, to: self) 77 | let innerSpacing: CGFloat = 2 78 | 79 | self.cutoutPath = { 80 | let cutoutRect = NSRect( 81 | x: referenceViewRect.origin.x - innerSpacing - cutoutPadding, 82 | y: referenceViewRect.origin.y - innerSpacing - cutoutPadding, 83 | width: referenceViewRect.size.width + 2 * innerSpacing + 2 * cutoutPadding, 84 | height: referenceViewRect.size.height + 2 * innerSpacing + 2 * cutoutPadding) 85 | 86 | let radius = 2 * innerSpacing + cutoutPadding 87 | 88 | return NSBezierPath(roundedRect: cutoutRect, xRadius: radius, yRadius: radius) 89 | }() 90 | } 91 | 92 | open override func draw(_ dirtyRect: NSRect) { 93 | 94 | guard let cutoutPath = cutoutPath else { return } 95 | guard let context = NSGraphicsContext.current else { return } 96 | 97 | overlayColor.setFill() 98 | bounds.fill() 99 | 100 | context.cgContext.setBlendMode(.destinationOut) 101 | NSColor.black.setFill() // 100% opacity for cutout 102 | cutoutPath.fill() 103 | 104 | context.cgContext.setBlendMode(.normal) 105 | } 106 | 107 | private var mouseMovedTrackingArea: NSTrackingArea? 108 | 109 | private func resetCursorOverride() { 110 | 111 | if let oldArea = mouseMovedTrackingArea { 112 | removeTrackingArea(oldArea) 113 | } 114 | 115 | let trackingArea = NSTrackingArea(rect: self.frame, options: [.activeInKeyWindow, .inVisibleRect, .mouseMoved], owner: self, userInfo: nil) 116 | addTrackingArea(trackingArea) 117 | mouseMovedTrackingArea = trackingArea 118 | 119 | // TODO: Known issue: When the overlay is triggered while the cursor is not an arrow, the cursor will not be set to be an arrow unless the mouse moves 120 | } 121 | 122 | open override func mouseMoved(with event: NSEvent) { 123 | NSCursor.arrow.set() 124 | } 125 | 126 | open override func mouseUp(with event: NSEvent) { } 127 | open override func mouseDown(with event: NSEvent) { } 128 | 129 | open override func keyDown(with event: NSEvent) { 130 | 131 | // Forwards events related to NSButton being key view up to the window 132 | if event.keyCode == 48 // Tab (switch key views) 133 | || event.keyCode == 49 { // Space 134 | super.keyDown(with: event) 135 | return 136 | } 137 | 138 | self.interpretKeyEvents([event]) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /AppGuideOverlay/OverlayViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import AppKit 4 | 5 | open class OverlayViewController: NSViewController { 6 | 7 | open weak var eventHandler: HandlesOverlayEvents? 8 | 9 | open var overlayView: OverlayView { 10 | get { return self.view as! OverlayView } 11 | set { self.view = newValue } 12 | } 13 | 14 | open var overlayColor: NSColor { 15 | get { return overlayView.overlayColor } 16 | set { overlayView.overlayColor = newValue } 17 | } 18 | 19 | open override func loadView() { 20 | 21 | let overlayView = OverlayView() 22 | overlayView.translatesAutoresizingMaskIntoConstraints = false 23 | self.overlayView = overlayView 24 | } 25 | 26 | private var appGuideStepIsLastInGuide = false 27 | 28 | open func display(appGuideStep: AppGuideStepViewModel) { 29 | 30 | overlayView.isHidden = false 31 | overlayView.display(appGuideStep: appGuideStep) 32 | appGuideStepIsLastInGuide = appGuideStep.isLastStep 33 | 34 | guard let overlainWindow = overlayView.window as? OverlainWindow else { return } 35 | overlainWindow.isDisplayingOverlay = true 36 | } 37 | 38 | open func hide() { 39 | 40 | overlayView.isHidden = true 41 | overlayView.hideOverlayStep() 42 | 43 | guard let overlainWindow = overlayView.window as? OverlainWindow else { return } 44 | overlainWindow.isDisplayingOverlay = false 45 | overlainWindow.makeFirstResponder(overlainWindow.initialFirstResponder) 46 | } 47 | 48 | // MARK: Events 49 | 50 | /// Responder chain callback from the "continue" button. 51 | @IBAction func nextStep(_ sender: Any?) { 52 | nextStep() 53 | } 54 | 55 | func nextStep() { 56 | eventHandler?.nextStep() 57 | } 58 | 59 | /// Responder chain callback from the "previous" button. 60 | @IBAction func previousStep(_ sender: Any?) { 61 | previousStep() 62 | } 63 | 64 | func previousStep() { 65 | eventHandler?.previousStep() 66 | } 67 | 68 | /// Responder chain callback from the "continue" button, now used as "complete". 69 | @IBAction func finishGuide(_ sender: Any?) { 70 | finish() 71 | } 72 | 73 | func finish() { 74 | eventHandler?.finish() 75 | } 76 | 77 | func cancel() { 78 | eventHandler?.cancel() 79 | } 80 | 81 | // MARK: - NSResponder Actions 82 | 83 | open override func moveRight(_ sender: Any?) { 84 | nextStep() 85 | } 86 | 87 | open override func moveLeft(_ sender: Any?) { 88 | previousStep() 89 | } 90 | 91 | open override func cancelOperation(_ sender: Any?) { 92 | cancel() 93 | } 94 | 95 | open override func insertNewline(_ sender: Any?) { 96 | continueOrFinish() 97 | } 98 | 99 | open override func insertNewlineIgnoringFieldEditor(_ sender: Any?) { 100 | continueOrFinish() 101 | } 102 | 103 | private func continueOrFinish() { 104 | if appGuideStepIsLastInGuide { 105 | finish() 106 | } else { 107 | nextStep() 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import Cocoa 4 | import AppGuideOverlay 5 | 6 | @NSApplicationMain 7 | class AppDelegate: NSObject, NSApplicationDelegate { 8 | 9 | @IBOutlet weak var window: OverlainWindow! 10 | 11 | @IBOutlet weak var textField: NSTextField! 12 | @IBOutlet weak var comboBox: NSComboBox! 13 | @IBOutlet weak var datePicker: NSDatePicker! 14 | @IBOutlet weak var button1: NSButton! 15 | @IBOutlet weak var button2: NSButton! 16 | 17 | var appGuideOverlay: AppGuideOverlay! 18 | 19 | func applicationDidFinishLaunching(_ aNotification: Notification) { 20 | 21 | let appGuide = AppGuide(steps: [ 22 | .init(title: "Omnia Sol Temperat", 23 | detail: "You can use this text field to configure everything \nto your heart's content. Just do it. Write, and go on. \nIt's simple. Very easy. You will like it.", 24 | position: .below, 25 | cutoutView: textField), 26 | .init(title: "Pressible", 27 | detail: "It is a button that can be \npressed for great effect.\n\nTry to resize the window\nwhile this is active!", 28 | position: .left, 29 | cutoutView: button1), 30 | .init(title: "Purus et Subtilis", 31 | detail: "Picking something from a limited list shouldn't be too hard.", 32 | position: .right, 33 | cutoutView: comboBox), 34 | .init(title: "Agenda", 35 | detail: "Dates and times are what drives people mad. It is grueling\nto endure this madness of space-time. Faciem aprilis. \nAd amorem properat, animus herilis.", 36 | position: .right, 37 | cutoutView: datePicker), 38 | .init(title: "Impressible", 39 | detail: "More buttons mean more fun, am I right?", 40 | position: .below, 41 | cutoutView: button2) 42 | ]) 43 | 44 | self.appGuideOverlay = AppGuideOverlay( 45 | appGuide: appGuide, 46 | appGuideSuperview: window.contentView!) 47 | appGuideOverlay.isFinishingAfterNext = false 48 | appGuideOverlay.delegate = self 49 | appGuideOverlay.start() 50 | } 51 | 52 | @IBAction func showOverlay(_ sender: Any?) { 53 | appGuideOverlay.start() 54 | } 55 | } 56 | 57 | extension AppDelegate: AppGuideOverlayDelegate { 58 | 59 | func appGuideWillAppear() { 60 | 61 | } 62 | 63 | func appGuideDidFinish() { 64 | 65 | } 66 | 67 | func appGuideDidCancel() { 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Example/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | Default 545 | 546 | 547 | 548 | 549 | 550 | 551 | Left to Right 552 | 553 | 554 | 555 | 556 | 557 | 558 | Right to Left 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | Default 570 | 571 | 572 | 573 | 574 | 575 | 576 | Left to Right 577 | 578 | 579 | 580 | 581 | 582 | 583 | Right to Left 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 709 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | Item 1 764 | Item 2 765 | Item 3 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | -------------------------------------------------------------------------------- /Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2018 Christian Tietze. All rights reserved. 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Christian Tietze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppGuideOverlay 2 | 3 | ![Swift 5.0](https://img.shields.io/badge/Swift-5.0-blue.svg?style=flat) 4 | ![Version](https://img.shields.io/github/tag/CleanCocoa/AppGuideOverlay.svg?style=flat) 5 | ![License](https://img.shields.io/github/license/CleanCocoa/AppGuideOverlay.svg?style=flat) 6 | ![Platform](https://img.shields.io/badge/platform-macOS-lightgrey.svg?style=flat) 7 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 8 | 9 | macOS user interface guide to display an overlay with descriptions of NSViews in your app. 10 | 11 | ## Usage 12 | 13 | ### Example 14 | 15 | Have a look at the example app that is part of this repository. The basic setup looks like this: 16 | 17 | ```swift 18 | // Given access to interesting view components ... 19 | let window: NSWindow = ... 20 | let textField: NSTextField = ... 21 | let button: NSButton = ... 22 | 23 | // ... and an AppGuide with steps attached to them ... 24 | let appGuide = AppGuide(steps: [ 25 | AppGuide.Step(title: "A Text Field", 26 | detail: "Use this to type something.", 27 | position: .below, 28 | cutoutView: textField), 29 | AppGuide.Step(title: "A Button", 30 | detail: "Press this to make something happen.", 31 | position: .right, 32 | cutoutView: button) 33 | ]) 34 | 35 | // ... keep the AppGuideOverlay around and start it: 36 | let appGuideOverlay = AppGuideOverlay( 37 | appGuide: appGuide, 38 | appGuideSuperview: window.contentView!) 39 | appGuideOverlay.start() 40 | ``` 41 | 42 | ### Types of Interest 43 | 44 | - `AppGuide` is the model. It contains an array of `AppGuide.Step`s that represents the steps in your guide. 45 | - `AppGuideOverlay` is a convenient service object to set up the app guide and control it. (You don't have to use it and can use `AppGuidePresenter` and a `HandlesOverlayEvents` conforming delegate. But `AppGuideOverlay` really is the most useful façade to get started.) 46 | - `AppGuideOverlayDelegate` is used to react to progress and completion events. 47 | 48 | ## Features 49 | 50 | - **Uses Auto Layout** to position the `AppGuide.Step` contents next to the view the steps explain. This ensures the window size fits the labels at all times. It also ensures the step's description is displayed correctly when the window is resized. 51 |
52 | 53 |
54 | - **Tasteful** pulsation of the cutout frame's size in the overlay. This also indicates that the app doesn't just hang. 55 |
56 | 57 |
58 | - **Good citizen:** No custom `NSRunLoop` or anything. It's just a view on top of other views that captures the first responder status and doesn't give it back to underlying controls. This means you can remote-control your user interface to change display content and animate as usual while the overlay is visible. 59 | 60 | ## Code License 61 | 62 | Copyright (c) 2018-2019 Christian Tietze. Distributed under the MIT License. 63 | 64 | - Uses [LoopingAnimation](https://github.com/CleanCocoa/LoopingAnimation) code directly, also distributed under the MIT License. 65 | -------------------------------------------------------------------------------- /img/auto-layout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CleanCocoa/AppGuideOverlay/69efdfae778fec747c5731c4292f8d7badcdbcff/img/auto-layout.gif -------------------------------------------------------------------------------- /img/breathing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CleanCocoa/AppGuideOverlay/69efdfae778fec747c5731c4292f8d7badcdbcff/img/breathing.gif --------------------------------------------------------------------------------