diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6bdcc5b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +Skassets \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b74adca5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log +Important changes to Instructions will be documented in this file. +Instructions will follow [Semantic Versioning](http://semver.org/) after reaching version 1.0.0. + +## [0.1.0](https://github.com/ephread/Instructions/releases/tag/0.1.0) +Released on 2015-10-01. + +### Added +- Initial release of Instructions. \ No newline at end of file diff --git a/Example/AppDelegate.swift b/Example/AppDelegate.swift index df422213..4d6912fd 100644 --- a/Example/AppDelegate.swift +++ b/Example/AppDelegate.swift @@ -1,10 +1,24 @@ +// AppDelegate.swift // -// AppDelegate.swift -// Instructions Example +// Copyright (c) 2015 Frédéric Maquin // -// Created by Frédéric Maquin on 26/09/15. -// Copyright © 2015 Ephread. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: // +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import UIKit @@ -40,7 +54,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationWillTerminate(application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } - - } - diff --git a/Example/Assets.xcassets/Contents.json b/Example/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Example/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/arrow-bottom.imageset/Contents.json b/Example/Assets.xcassets/arrow-bottom.imageset/Contents.json new file mode 100644 index 00000000..caeeec56 --- /dev/null +++ b/Example/Assets.xcassets/arrow-bottom.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "arrow-bottom.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "arrow-bottom@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "arrow-bottom@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/arrow-bottom.imageset/arrow-bottom.png b/Example/Assets.xcassets/arrow-bottom.imageset/arrow-bottom.png new file mode 100644 index 00000000..3a4a1c9b Binary files /dev/null and b/Example/Assets.xcassets/arrow-bottom.imageset/arrow-bottom.png differ diff --git a/Example/Assets.xcassets/arrow-bottom.imageset/arrow-bottom@2x.png b/Example/Assets.xcassets/arrow-bottom.imageset/arrow-bottom@2x.png new file mode 100644 index 00000000..22ac006e Binary files /dev/null and b/Example/Assets.xcassets/arrow-bottom.imageset/arrow-bottom@2x.png differ diff --git a/Example/Assets.xcassets/arrow-bottom.imageset/arrow-bottom@3x.png b/Example/Assets.xcassets/arrow-bottom.imageset/arrow-bottom@3x.png new file mode 100644 index 00000000..3aacf802 Binary files /dev/null and b/Example/Assets.xcassets/arrow-bottom.imageset/arrow-bottom@3x.png differ diff --git a/Example/Assets.xcassets/arrow-top.imageset/Contents.json b/Example/Assets.xcassets/arrow-top.imageset/Contents.json new file mode 100644 index 00000000..cc871864 --- /dev/null +++ b/Example/Assets.xcassets/arrow-top.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "arrow-top.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "arrow-top@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "arrow-top@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/arrow-top.imageset/arrow-top.png b/Example/Assets.xcassets/arrow-top.imageset/arrow-top.png new file mode 100644 index 00000000..239a3447 Binary files /dev/null and b/Example/Assets.xcassets/arrow-top.imageset/arrow-top.png differ diff --git a/Example/Assets.xcassets/arrow-top.imageset/arrow-top@2x.png b/Example/Assets.xcassets/arrow-top.imageset/arrow-top@2x.png new file mode 100644 index 00000000..a0f7e213 Binary files /dev/null and b/Example/Assets.xcassets/arrow-top.imageset/arrow-top@2x.png differ diff --git a/Example/Assets.xcassets/arrow-top.imageset/arrow-top@3x.png b/Example/Assets.xcassets/arrow-top.imageset/arrow-top@3x.png new file mode 100644 index 00000000..051f892c Binary files /dev/null and b/Example/Assets.xcassets/arrow-top.imageset/arrow-top@3x.png differ diff --git a/Example/Assets.xcassets/button-background-highlighted.imageset/Contents.json b/Example/Assets.xcassets/button-background-highlighted.imageset/Contents.json new file mode 100644 index 00000000..e7dacce0 --- /dev/null +++ b/Example/Assets.xcassets/button-background-highlighted.imageset/Contents.json @@ -0,0 +1,65 @@ +{ + "images" : [ + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 6, + "top" : 5, + "right" : 5, + "left" : 5 + } + }, + "idiom" : "universal", + "filename" : "button-background-highlighted.png", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 2, + "height" : 2 + }, + "cap-insets" : { + "bottom" : 12, + "top" : 10, + "right" : 10, + "left" : 10 + } + }, + "idiom" : "universal", + "filename" : "button-background-highlighted@2x.png", + "scale" : "2x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 18, + "top" : 17, + "right" : 16, + "left" : 16 + } + }, + "idiom" : "universal", + "filename" : "button-background-highlighted@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/button-background-highlighted.imageset/button-background-highlighted.png b/Example/Assets.xcassets/button-background-highlighted.imageset/button-background-highlighted.png new file mode 100644 index 00000000..340acb7b Binary files /dev/null and b/Example/Assets.xcassets/button-background-highlighted.imageset/button-background-highlighted.png differ diff --git a/Example/Assets.xcassets/button-background-highlighted.imageset/button-background-highlighted@2x.png b/Example/Assets.xcassets/button-background-highlighted.imageset/button-background-highlighted@2x.png new file mode 100644 index 00000000..38a4335f Binary files /dev/null and b/Example/Assets.xcassets/button-background-highlighted.imageset/button-background-highlighted@2x.png differ diff --git a/Example/Assets.xcassets/button-background-highlighted.imageset/button-background-highlighted@3x.png b/Example/Assets.xcassets/button-background-highlighted.imageset/button-background-highlighted@3x.png new file mode 100644 index 00000000..374ca2cc Binary files /dev/null and b/Example/Assets.xcassets/button-background-highlighted.imageset/button-background-highlighted@3x.png differ diff --git a/Example/Assets.xcassets/button-background.imageset/Contents.json b/Example/Assets.xcassets/button-background.imageset/Contents.json new file mode 100644 index 00000000..d3882ae3 --- /dev/null +++ b/Example/Assets.xcassets/button-background.imageset/Contents.json @@ -0,0 +1,65 @@ +{ + "images" : [ + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 6, + "top" : 5, + "right" : 5, + "left" : 5 + } + }, + "idiom" : "universal", + "filename" : "button-background.png", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 2, + "height" : 2 + }, + "cap-insets" : { + "bottom" : 12, + "top" : 10, + "right" : 10, + "left" : 10 + } + }, + "idiom" : "universal", + "filename" : "button-background@2x.png", + "scale" : "2x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 18, + "top" : 17, + "right" : 16, + "left" : 16 + } + }, + "idiom" : "universal", + "filename" : "button-background@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/button-background.imageset/button-background.png b/Example/Assets.xcassets/button-background.imageset/button-background.png new file mode 100644 index 00000000..8316f97a Binary files /dev/null and b/Example/Assets.xcassets/button-background.imageset/button-background.png differ diff --git a/Example/Assets.xcassets/button-background.imageset/button-background@2x.png b/Example/Assets.xcassets/button-background.imageset/button-background@2x.png new file mode 100644 index 00000000..c04a04cc Binary files /dev/null and b/Example/Assets.xcassets/button-background.imageset/button-background@2x.png differ diff --git a/Example/Assets.xcassets/button-background.imageset/button-background@3x.png b/Example/Assets.xcassets/button-background.imageset/button-background@3x.png new file mode 100644 index 00000000..668a6724 Binary files /dev/null and b/Example/Assets.xcassets/button-background.imageset/button-background@3x.png differ diff --git a/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/Contents.json b/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/Contents.json new file mode 100644 index 00000000..aa254782 --- /dev/null +++ b/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "tile", + "width" : 1 + }, + "cap-insets" : { + "right" : 8, + "left" : 8 + } + }, + "idiom" : "universal", + "filename" : "coach-mark-bottom-plate.png", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "tile", + "width" : 2 + }, + "cap-insets" : { + "right" : 16, + "left" : 16 + } + }, + "idiom" : "universal", + "filename" : "coach-mark-bottom-plate@2x.png", + "scale" : "2x" + }, + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "tile", + "width" : 1 + }, + "cap-insets" : { + "right" : 25, + "left" : 25 + } + }, + "idiom" : "universal", + "filename" : "coach-mark-bottom-plate@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/coach-mark-bottom-plate.png b/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/coach-mark-bottom-plate.png new file mode 100644 index 00000000..7b82065a Binary files /dev/null and b/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/coach-mark-bottom-plate.png differ diff --git a/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/coach-mark-bottom-plate@2x.png b/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/coach-mark-bottom-plate@2x.png new file mode 100644 index 00000000..13d86fd2 Binary files /dev/null and b/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/coach-mark-bottom-plate@2x.png differ diff --git a/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/coach-mark-bottom-plate@3x.png b/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/coach-mark-bottom-plate@3x.png new file mode 100644 index 00000000..c41f2bb9 Binary files /dev/null and b/Example/Assets.xcassets/coach-mark-bottom-plate.imageset/coach-mark-bottom-plate@3x.png differ diff --git a/Example/Assets.xcassets/coach-mark-top-plate.imageset/Contents.json b/Example/Assets.xcassets/coach-mark-top-plate.imageset/Contents.json new file mode 100644 index 00000000..cd7a24eb --- /dev/null +++ b/Example/Assets.xcassets/coach-mark-top-plate.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "tile", + "width" : 1 + }, + "cap-insets" : { + "right" : 8, + "left" : 8 + } + }, + "idiom" : "universal", + "filename" : "coach-mark-top-plate.png", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "tile", + "width" : 2 + }, + "cap-insets" : { + "right" : 16, + "left" : 16 + } + }, + "idiom" : "universal", + "filename" : "coach-mark-top-plate@2x.png", + "scale" : "2x" + }, + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "tile", + "width" : 1 + }, + "cap-insets" : { + "right" : 25, + "left" : 25 + } + }, + "idiom" : "universal", + "filename" : "coach-mark-top-plate@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/coach-mark-top-plate.imageset/coach-mark-top-plate.png b/Example/Assets.xcassets/coach-mark-top-plate.imageset/coach-mark-top-plate.png new file mode 100644 index 00000000..ab9e9d48 Binary files /dev/null and b/Example/Assets.xcassets/coach-mark-top-plate.imageset/coach-mark-top-plate.png differ diff --git a/Example/Assets.xcassets/coach-mark-top-plate.imageset/coach-mark-top-plate@2x.png b/Example/Assets.xcassets/coach-mark-top-plate.imageset/coach-mark-top-plate@2x.png new file mode 100644 index 00000000..754a1c88 Binary files /dev/null and b/Example/Assets.xcassets/coach-mark-top-plate.imageset/coach-mark-top-plate@2x.png differ diff --git a/Example/Assets.xcassets/coach-mark-top-plate.imageset/coach-mark-top-plate@3x.png b/Example/Assets.xcassets/coach-mark-top-plate.imageset/coach-mark-top-plate@3x.png new file mode 100644 index 00000000..1e9c1e01 Binary files /dev/null and b/Example/Assets.xcassets/coach-mark-top-plate.imageset/coach-mark-top-plate@3x.png differ diff --git a/Example/Assets.xcassets/face-female.imageset/Contents.json b/Example/Assets.xcassets/face-female.imageset/Contents.json new file mode 100644 index 00000000..452d8bc0 --- /dev/null +++ b/Example/Assets.xcassets/face-female.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "face-female.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "face-female@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "face-female@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/face-female.imageset/face-female.png b/Example/Assets.xcassets/face-female.imageset/face-female.png new file mode 100644 index 00000000..1845c652 Binary files /dev/null and b/Example/Assets.xcassets/face-female.imageset/face-female.png differ diff --git a/Example/Assets.xcassets/face-female.imageset/face-female@2x.png b/Example/Assets.xcassets/face-female.imageset/face-female@2x.png new file mode 100644 index 00000000..04be27fd Binary files /dev/null and b/Example/Assets.xcassets/face-female.imageset/face-female@2x.png differ diff --git a/Example/Assets.xcassets/face-female.imageset/face-female@3x.png b/Example/Assets.xcassets/face-female.imageset/face-female@3x.png new file mode 100644 index 00000000..e8f68a13 Binary files /dev/null and b/Example/Assets.xcassets/face-female.imageset/face-female@3x.png differ diff --git a/Example/Assets.xcassets/face-male.imageset/Contents.json b/Example/Assets.xcassets/face-male.imageset/Contents.json new file mode 100644 index 00000000..3c7b74f6 --- /dev/null +++ b/Example/Assets.xcassets/face-male.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "face-male.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "face-male@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "face-male@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/face-male.imageset/face-male.png b/Example/Assets.xcassets/face-male.imageset/face-male.png new file mode 100644 index 00000000..1b7f193b Binary files /dev/null and b/Example/Assets.xcassets/face-male.imageset/face-male.png differ diff --git a/Example/Assets.xcassets/face-male.imageset/face-male@2x.png b/Example/Assets.xcassets/face-male.imageset/face-male@2x.png new file mode 100644 index 00000000..3878769e Binary files /dev/null and b/Example/Assets.xcassets/face-male.imageset/face-male@2x.png differ diff --git a/Example/Assets.xcassets/face-male.imageset/face-male@3x.png b/Example/Assets.xcassets/face-male.imageset/face-male@3x.png new file mode 100644 index 00000000..4a9c9b05 Binary files /dev/null and b/Example/Assets.xcassets/face-male.imageset/face-male@3x.png differ diff --git a/Example/Base.lproj/LaunchScreen.storyboard b/Example/Base.lproj/LaunchScreen.storyboard index 2e721e18..dd3a2b15 100644 --- a/Example/Base.lproj/LaunchScreen.storyboard +++ b/Example/Base.lproj/LaunchScreen.storyboard @@ -1,7 +1,7 @@ - + - + @@ -15,8 +15,21 @@ + + + + + + + diff --git a/Example/Base.lproj/Main.storyboard b/Example/Base.lproj/Main.storyboard index db6d892e..57c3816e 100644 --- a/Example/Base.lproj/Main.storyboard +++ b/Example/Base.lproj/Main.storyboard @@ -1,25 +1,867 @@ - + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + diff --git a/Example/BlurringOverlayViewController.swift b/Example/BlurringOverlayViewController.swift new file mode 100644 index 00000000..44f97179 --- /dev/null +++ b/Example/BlurringOverlayViewController.swift @@ -0,0 +1,34 @@ +// BlurringOverlayViewController.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +/// Will display coach marks on top of a blurred background. +internal class BlurringOverlayViewController: DefaultViewController { + //MARK: - View lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.coachMarksController?.overlayBlurEffectStyle = .Dark + } +} \ No newline at end of file diff --git a/Example/CustomCoachMarkArrowView.swift b/Example/CustomCoachMarkArrowView.swift new file mode 100644 index 00000000..07edde06 --- /dev/null +++ b/Example/CustomCoachMarkArrowView.swift @@ -0,0 +1,74 @@ +// CustomCoachMarkArrowView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +// Custom coach mark body (with the secret-like arrow) +internal class CustomCoachMarkArrowView : UIView, CoachMarkArrowView { + //MARK: - Internal properties + var topPlateImage = UIImage(named: "coach-mark-top-plate") + var bottomPlateImage = UIImage(named: "coach-mark-bottom-plate") + var plate = UIImageView() + + var highlighted: Bool = false + + //MARK: - Private properties + private var column = UIView() + + //MARK: - Initialization + init?(orientation: CoachMarkArrowOrientation) { + super.init(frame: CGRectZero) + + if orientation == .Top { + self.plate.image = topPlateImage + } else { + self.plate.image = bottomPlateImage + } + + self.translatesAutoresizingMaskIntoConstraints = false + self.column.translatesAutoresizingMaskIntoConstraints = false + self.plate.translatesAutoresizingMaskIntoConstraints = false + + self.addSubview(plate) + self.addSubview(column) + + plate.backgroundColor = UIColor.clearColor() + column.backgroundColor = UIColor.whiteColor() + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[plate]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["plate" : plate])) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[column(==3)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["column" : column])) + + self.addConstraint(NSLayoutConstraint(item: column, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 1, constant: 0)) + + if orientation == .Top { + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[plate(==5)][column(==10)]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["plate" : plate, "column" : column])) + } else { + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[column(==10)][plate(==5)]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["plate" : plate, "column" : column])) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("This class does not support NSCoding.") + } +} diff --git a/Example/CustomCoachMarkBodyView.swift b/Example/CustomCoachMarkBodyView.swift new file mode 100644 index 00000000..0ddfe2c2 --- /dev/null +++ b/Example/CustomCoachMarkBodyView.swift @@ -0,0 +1,99 @@ +// CustomCoachMarkBodyView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +// Custom coach mark body (with the secret-like arrow) +internal class CustomCoachMarkBodyView : UIView, CoachMarkBodyView { + //MARK: - Internal properties + var nextControl: UIControl? { + get { + return self.nextButton + } + } + + var highlighted: Bool = false + + var nextButton = UIButton() + var hintLabel = UITextView(); + + weak var highlightArrowDelegate: CoachMarkBodyHighlightArrowDelegate? = nil + + // MARK: - Initialization + override init (frame: CGRect) { + super.init(frame: frame) + + self.setupInnerViewHierarchy() + } + + convenience init() { + self.init(frame: CGRectZero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("This class does not support NSCoding.") + } + + //MARK: - Private methods + private func setupInnerViewHierarchy() { + self.translatesAutoresizingMaskIntoConstraints = false + self.backgroundColor = UIColor.whiteColor() + + self.clipsToBounds = true + self.layer.cornerRadius = 4 + + self.hintLabel.backgroundColor = UIColor.clearColor() + self.hintLabel.textColor = UIColor.darkGrayColor() + self.hintLabel.font = UIFont.systemFontOfSize(15.0) + self.hintLabel.scrollEnabled = false; + self.hintLabel.textAlignment = .Justified; + self.hintLabel.layoutManager.hyphenationFactor = 2.0; + self.hintLabel.editable = false; + + self.nextButton.translatesAutoresizingMaskIntoConstraints = false; + self.hintLabel.translatesAutoresizingMaskIntoConstraints = false; + + self.nextButton.userInteractionEnabled = true; + self.hintLabel.userInteractionEnabled = false; + + self.nextButton.setBackgroundImage(UIImage(named: "button-background"), forState: .Normal) + self.nextButton.setBackgroundImage(UIImage(named: "button-background-highlighted"), forState: .Highlighted) + + self.nextButton.setTitleColor(UIColor.whiteColor(), forState: .Normal) + self.nextButton.titleLabel?.font = UIFont.systemFontOfSize(15.0) + + self.addSubview(nextButton); + self.addSubview(hintLabel); + + self.addConstraint(NSLayoutConstraint(item: nextButton, attribute: .CenterY, relatedBy: .Equal, toItem: self, attribute: .CenterY, multiplier: 1, constant: 0)) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[nextButton(==30)]", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["nextButton": nextButton])) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(5)-[hintLabel]-(5)-|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["hintLabel": hintLabel])) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(10)-[hintLabel]-(10)-[nextButton(==40)]-(10)-|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["hintLabel": hintLabel, "nextButton": nextButton])) + } +} \ No newline at end of file diff --git a/Example/CustomViewController.swift b/Example/CustomViewController.swift new file mode 100644 index 00000000..a953d64f --- /dev/null +++ b/Example/CustomViewController.swift @@ -0,0 +1,141 @@ +// CustomViewsViewController.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +// Will display custom coach marks. +internal class CustomViewsViewController: ProfileViewController, CoachMarksControllerDataSource { + + //MARK: - IBOutlet + @IBOutlet var infoStackView: UIStackView? + + //MARK: - View Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.coachMarksController?.datasource = self + + self.coachMarksController?.overlayBackgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.5) + } + + //MARK: - Protocol Conformance | CoachMarksControllerDataSource + func numberOfCoachMarksForCoachMarksController(coachMarksController: CoachMarksController) -> Int { + return 5 + } + + func coachMarksController(coachMarksController: CoachMarksController, coachMarksForIndex index: Int) -> CoachMark { + + // This will create cutout path matching perfectly the given view. + // No padding! + let flatBezierPathBlock = { (frame: CGRect) -> UIBezierPath in + return UIBezierPath(rect: frame) + } + + var coachMark : CoachMark; + + switch(index) { + case 0: + coachMark = coachMarksController.coachMarkForView(self.avatar) { (frame: CGRect) -> UIBezierPath in + // This will create a circular cutoutPath, perfect for the circular avatar! + return UIBezierPath(ovalInRect: CGRectInset(frame, -4, -4)) + } + case 1: + coachMark = coachMarksController.coachMarkForView(self.handleLabel) + case 2: + coachMark = coachMarksController.coachMarkForView(self.infoStackView, pointOfInterest: self.emailLabel?.center, bezierPathBlock: flatBezierPathBlock) + case 3: + coachMark = coachMarksController.coachMarkForView(self.infoStackView, pointOfInterest: self.postsLabel?.center, bezierPathBlock: flatBezierPathBlock) + case 4: + coachMark = coachMarksController.coachMarkForView(self.infoStackView, pointOfInterest: self.reputationLabel?.center, bezierPathBlock: flatBezierPathBlock) + default: + coachMark = coachMarksController.coachMarkForView() + } + + coachMark.gapBetweenCoachMarkAndCutoutPath = 6.0 + + return coachMark + } + + func coachMarksController(coachMarksController: CoachMarksController, coachMarkViewsForIndex index: Int, coachMark: CoachMark) -> (bodyView: CoachMarkBodyView, arrowView: CoachMarkArrowView?) { + + let coachMarkBodyView = CustomCoachMarkBodyView() + var coachMarkArrowView: CustomCoachMarkArrowView? = nil + + var width: CGFloat = 0.0 + + switch(index) { + case 0: + coachMarkBodyView.hintLabel.text = self.avatarText + coachMarkBodyView.nextButton.setTitle(self.nextButtonText, forState: .Normal) + + if let avatar = self.avatar { + width = avatar.bounds.width + } + case 1: + coachMarkBodyView.hintLabel.text = self.handleText + coachMarkBodyView.nextButton.setTitle(self.nextButtonText, forState: .Normal) + + if let handleLabel = self.handleLabel { + width = handleLabel.bounds.width + } + case 2: + coachMarkBodyView.hintLabel.text = self.emailText + coachMarkBodyView.nextButton.setTitle(self.nextButtonText, forState: .Normal) + + if let emailLabel = self.emailLabel { + width = emailLabel.bounds.width + } + case 3: + coachMarkBodyView.hintLabel.text = self.postsText + coachMarkBodyView.nextButton.setTitle(self.nextButtonText, forState: .Normal) + + if let postsLabel = self.postsLabel { + width = postsLabel.bounds.width + } + case 4: + coachMarkBodyView.hintLabel.text = self.reputationText + coachMarkBodyView.nextButton.setTitle(self.nextButtonText, forState: .Normal) + + if let reputationLabel = self.reputationLabel { + width = reputationLabel.bounds.width + } + default: break + } + + // We create an arrow only if an orientation is provided (i. e., a cutoutPath is provided). + // For that custom coachmark, we'll need to update a bit the arrow, so it'll look like + // it fits the width of the view. + if let arrowOrientation = coachMark.arrowOrientation { + coachMarkArrowView = CustomCoachMarkArrowView(orientation: arrowOrientation) + + // If the view is larger than 1/3 of the overlay width, we'll shrink a bit the width + // of the arrow. + let oneThirdOfWidth = coachMarksController.view.bounds.size.width / 3 + let adjustedWidth = width >= oneThirdOfWidth ? width - 2 * coachMark.horizontalMargin : width + + coachMarkArrowView!.plate.addConstraint(NSLayoutConstraint(item: coachMarkArrowView!.plate, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: adjustedWidth)) + } + + return (bodyView: coachMarkBodyView, arrowView: coachMarkArrowView) + } +} diff --git a/Example/DefaultViewController.swift b/Example/DefaultViewController.swift new file mode 100644 index 00000000..8edf8752 --- /dev/null +++ b/Example/DefaultViewController.swift @@ -0,0 +1,91 @@ +// DefaultViewController.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +// That's the default controller, using every defaults made available by Instructions. +// It can't get any simpler. +internal class DefaultViewController: ProfileViewController, CoachMarksControllerDataSource { + //MARK: - View Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.coachMarksController?.datasource = self + + self.emailLabel?.layer.cornerRadius = 4.0 + self.postsLabel?.layer.cornerRadius = 4.0 + self.reputationLabel?.layer.cornerRadius = 4.0 + } + + //MARK: - Protocol Conformance | CoachMarksControllerDataSource + func numberOfCoachMarksForCoachMarksController(coachMarksController: CoachMarksController) -> Int { + return 5 + } + + func coachMarksController(coachMarksController: CoachMarksController, coachMarksForIndex index: Int) -> CoachMark { + switch(index) { + case 0: + return coachMarksController.coachMarkForView(self.navigationController?.navigationBar) { (frame: CGRect) -> UIBezierPath in + // This will make a cutoutPath matching the shape of + // the component (no padding, no rounded corners). + return UIBezierPath(rect: frame) + } + case 1: + return coachMarksController.coachMarkForView(self.handleLabel) + case 2: + return coachMarksController.coachMarkForView(self.emailLabel) + case 3: + return coachMarksController.coachMarkForView(self.postsLabel) + case 4: + return coachMarksController.coachMarkForView(self.reputationLabel) + default: + return coachMarksController.coachMarkForView() + } + } + + func coachMarksController(coachMarksController: CoachMarksController, coachMarkViewsForIndex index: Int, coachMark: CoachMark) -> (bodyView: CoachMarkBodyView, arrowView: CoachMarkArrowView?) { + + let coachViews = coachMarksController.defaultCoachViewsWithArrow(true, arrowOrientation: coachMark.arrowOrientation) + + switch(index) { + case 0: + coachViews.bodyView.hintLabel.text = self.profileSectionText + coachViews.bodyView.nextLabel.text = self.nextButtonText + case 1: + coachViews.bodyView.hintLabel.text = self.handleText + coachViews.bodyView.nextLabel.text = self.nextButtonText + case 2: + coachViews.bodyView.hintLabel.text = self.emailText + coachViews.bodyView.nextLabel.text = self.nextButtonText + case 3: + coachViews.bodyView.hintLabel.text = self.postsText + coachViews.bodyView.nextLabel.text = self.nextButtonText + case 4: + coachViews.bodyView.hintLabel.text = self.reputationText + coachViews.bodyView.nextLabel.text = self.nextButtonText + default: break + } + + return (bodyView: coachViews.bodyView, arrowView: coachViews.arrowView) + } +} diff --git a/Example/DelegateViewController.swift b/Example/DelegateViewController.swift new file mode 100644 index 00000000..5ea1042a --- /dev/null +++ b/Example/DelegateViewController.swift @@ -0,0 +1,139 @@ +// DelegatetViewController.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +// This class show off the oportunities provided by the delegate mechanism. +internal class DelegatetViewController: ProfileViewController, CoachMarksControllerDataSource, CoachMarksControllerDelegate { + + //MARK: - IBOutlet + @IBOutlet var profileBackgroundView: UIView? + @IBOutlet var avatarVerticalPositionConstraint: NSLayoutConstraint? + + //MARK: - View Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.coachMarksController?.delegate = self + self.coachMarksController?.datasource = self + + self.emailLabel?.layer.cornerRadius = 4.0 + self.postsLabel?.layer.cornerRadius = 4.0 + self.reputationLabel?.layer.cornerRadius = 4.0 + } + + //MARK: - Protocol Conformance | CoachMarksControllerDataSource + func numberOfCoachMarksForCoachMarksController(coachMarksController: CoachMarksController) -> Int { + return 5 + } + + func coachMarksController(coachMarksController: CoachMarksController, coachMarksForIndex index: Int) -> CoachMark { + switch(index) { + case 0: + return coachMarksController.coachMarkForView(self.avatar) { (frame: CGRect) -> UIBezierPath in + return UIBezierPath(ovalInRect: CGRectInset(frame, -4, -4)) + } + case 1: + return coachMarksController.coachMarkForView(self.handleLabel) + case 2: + return coachMarksController.coachMarkForView(self.emailLabel) + case 3: + return coachMarksController.coachMarkForView(self.postsLabel) + case 4: + return coachMarksController.coachMarkForView(self.reputationLabel) + default: + return coachMarksController.coachMarkForView() + } + } + + func coachMarksController(coachMarksController: CoachMarksController, coachMarkViewsForIndex index: Int, coachMark: CoachMark) -> (bodyView: CoachMarkBodyView, arrowView: CoachMarkArrowView?) { + + let coachViews = coachMarksController.defaultCoachViewsWithArrow(true, arrowOrientation: coachMark.arrowOrientation) + + switch(index) { + case 0: + coachViews.bodyView.hintLabel.text = self.avatarText + coachViews.bodyView.nextLabel.text = self.nextButtonText + case 1: + coachViews.bodyView.hintLabel.text = self.handleText + coachViews.bodyView.nextLabel.text = self.nextButtonText + case 2: + coachViews.bodyView.hintLabel.text = self.emailText + coachViews.bodyView.nextLabel.text = self.nextButtonText + case 3: + coachViews.bodyView.hintLabel.text = self.postsText + coachViews.bodyView.nextLabel.text = self.nextButtonText + case 4: + coachViews.bodyView.hintLabel.text = self.reputationText + coachViews.bodyView.nextLabel.text = self.nextButtonText + default: break + } + + return (bodyView: coachViews.bodyView, arrowView: coachViews.arrowView) + } + + //MARK: - Protocol Conformance | CoachMarksControllerDelegate + func coachMarksController(coachMarksController: CoachMarksController, inout coachMarkWillShow coachMark: CoachMark, forIndex index: Int) { + if index == 0 { + // We'll need to play an animation before showing up the coach mark. + // To be able to play the animation and then show the coach mark and not stall + // the UI (i. e. keep the asynchronicity), we'll pause the controller. + coachMarksController.pause() + + // Then we run the animation. + self.avatarVerticalPositionConstraint?.constant = 30 + self.view.needsUpdateConstraints() + + UIView.animateWithDuration(1, animations: { () -> Void in + self.view.layoutIfNeeded() + }, completion: { (finished: Bool) -> Void in + + // Once the animation is completed, we update the coach mark, + // and start the display again. + coachMarksController.updateCurrentCoachMarkForView(self.avatar, pointOfInterest: nil) { + (frame: CGRect) -> UIBezierPath in + return UIBezierPath(ovalInRect: CGRectInset(frame, -4, -4)) + } + + coachMarksController.play() + }) + } + } + + func coachMarksController(coachMarksController: CoachMarksController, coachMarkWillDisappear coachMark: CoachMark, forIndex index: Int) { + if index == 1 { + self.avatarVerticalPositionConstraint?.constant = 0 + self.view.needsUpdateConstraints() + + UIView.animateWithDuration(1, animations: { () -> Void in + self.view.layoutIfNeeded() + }) + } + } + + func didFinishShowingFromCoachMarksController(coachMarksController: CoachMarksController) { + UIView.animateWithDuration(1, animations: { () -> Void in + self.profileBackgroundView?.backgroundColor = UIColor(red: 244.0/255.0, green: 126.0/255.0, blue: 46.0/255.0, alpha: 1.0) + }) + } +} \ No newline at end of file diff --git a/Example/MixedCoachMarksViewController.swift b/Example/MixedCoachMarksViewController.swift new file mode 100644 index 00000000..ea9c1b02 --- /dev/null +++ b/Example/MixedCoachMarksViewController.swift @@ -0,0 +1,132 @@ +// MixedCoachMarksViewsViewController.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +// This class mix different kind of coach marks together. +internal class MixedCoachMarksViewsViewController: ProfileViewController, CoachMarksControllerDataSource { + + //MARK: - Private properties + private let swipeImage = UIImage(named: "swipe") + + //MARK: - View Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.coachMarksController?.datasource = self + + self.coachMarksController?.overlayBackgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.5) + } + + //MARK: - Protocol Conformance | CoachMarksControllerDataSource + func numberOfCoachMarksForCoachMarksController(coachMarksController: CoachMarksController) -> Int { + return 4 + } + + func coachMarksController(coachMarksController: CoachMarksController, coachMarksForIndex index: Int) -> CoachMark { + + var coachMark : CoachMark; + + switch(index) { + case 0: + coachMark = coachMarksController.coachMarkForView(self.handleLabel) + case 1: + coachMark = coachMarksController.coachMarkForView(self.emailLabel) + case 2: + coachMark = coachMarksController.coachMarkForView(self.postsLabel) + case 3: + coachMark = coachMarksController.coachMarkForView(self.reputationLabel) + default: + coachMark = coachMarksController.coachMarkForView() + } + + coachMark.gapBetweenCoachMarkAndCutoutPath = 6.0 + + return coachMark + } + + func coachMarksController(coachMarksController: CoachMarksController, coachMarkViewsForIndex index: Int, coachMark: CoachMark) -> (bodyView: CoachMarkBodyView, arrowView: CoachMarkArrowView?) { + + var bodyView : CoachMarkBodyView + var arrowView : CoachMarkArrowView? + + switch(index) { + case 0: + let coachMarkBodyView = CustomCoachMarkBodyView() + var coachMarkArrowView: CustomCoachMarkArrowView? = nil + + coachMarkBodyView.hintLabel.text = self.handleText + coachMarkBodyView.nextButton.setTitle(self.nextButtonText, forState: .Normal) + + var width: CGFloat = 0.0 + + if let handleLabel = self.handleLabel { + width = handleLabel.bounds.width + } + + if let arrowOrientation = coachMark.arrowOrientation { + coachMarkArrowView = CustomCoachMarkArrowView(orientation: arrowOrientation) + + coachMarkArrowView!.plate.addConstraint(NSLayoutConstraint(item: coachMarkArrowView!.plate, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: width)) + } + + bodyView = coachMarkBodyView + arrowView = coachMarkArrowView + case 1: + let coachViews = coachMarksController.defaultCoachViewsWithArrow(true, arrowOrientation: coachMark.arrowOrientation) + + coachViews.bodyView.hintLabel.text = self.emailText + coachViews.bodyView.nextLabel.text = self.nextButtonText + + bodyView = coachViews.bodyView + arrowView = coachViews.arrowView + case 2: + let coachViews = coachMarksController.defaultCoachViewsWithArrow(true, arrowOrientation: coachMark.arrowOrientation) + + coachViews.bodyView.hintLabel.text = self.postsText + coachViews.bodyView.nextLabel.text = self.nextButtonText + + bodyView = coachViews.bodyView + arrowView = coachViews.arrowView + case 3: + let coachMarkBodyView = TransparentCoachMarkBodyView() + var coachMarkArrowView: TransparentCoachMarkArrowView? = nil + + coachMarkBodyView.hintLabel.text = self.handleText + + if let arrowOrientation = coachMark.arrowOrientation { + coachMarkArrowView = TransparentCoachMarkArrowView(orientation: arrowOrientation) + } + + bodyView = coachMarkBodyView + arrowView = coachMarkArrowView + default: + let coachViews = coachMarksController.defaultCoachViewsWithArrow(true, arrowOrientation: coachMark.arrowOrientation) + + bodyView = coachViews.bodyView + arrowView = coachViews.arrowView + } + + return (bodyView: bodyView, arrowView: arrowView) + } +} \ No newline at end of file diff --git a/Example/ProfileViewController.swift b/Example/ProfileViewController.swift new file mode 100644 index 00000000..431d0519 --- /dev/null +++ b/Example/ProfileViewController.swift @@ -0,0 +1,61 @@ +// ProfileViewController.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +/// This class serves as a base for all the other examples +internal class ProfileViewController: UIViewController { + //MARK: - IBOutlet + @IBOutlet var handleLabel: UILabel? + @IBOutlet var emailLabel: UILabel? + @IBOutlet var postsLabel: UILabel? + @IBOutlet var reputationLabel: UILabel? + @IBOutlet var avatar: UIImageView? + + //MARK: - Public properties + var coachMarksController: CoachMarksController? + + let avatarText = "That's your profile picture. You look gorgeous!" + let profileSectionText = "You are in the profile section, where you can review all your informations." + let handleText = "That, here, is your name. Sounds a bit generic, don't you think?" + let emailText = "This is your email address. Nothing too fancy." + let postsText = "Here, is the number of posts you made. You are just starting up!" + let reputationText = "That's your reputation around here, that's actually quite good." + + let nextButtonText = "Ok!" + + //MARK: - View lifecycle + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + + self.coachMarksController = CoachMarksController() + self.coachMarksController?.allowOverlayTap = true + } + + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + + self.coachMarksController?.startOn(self.navigationController!) + } +} diff --git a/Example/TransparentCoachMarkArrowView.swift b/Example/TransparentCoachMarkArrowView.swift new file mode 100644 index 00000000..9af978bd --- /dev/null +++ b/Example/TransparentCoachMarkArrowView.swift @@ -0,0 +1,50 @@ +// TransparentCoachMarkArrowView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +// Transparent coach mark (text without background, cool arrow) +internal class TransparentCoachMarkArrowView : UIImageView, CoachMarkArrowView { + //MARK: - Initialization + init(orientation: CoachMarkArrowOrientation) { + if orientation == .Top { + super.init(image: UIImage(named: "arrow-top")) + } else { + super.init(image: UIImage(named: "arrow-bottom")) + } + + self.translatesAutoresizingMaskIntoConstraints = false + + self.addConstraint(NSLayoutConstraint(item: self, attribute: .Width, relatedBy: .Equal, + toItem: nil, attribute: .NotAnAttribute, + multiplier: 1, constant: self.image!.size.width)) + + self.addConstraint(NSLayoutConstraint(item: self, attribute: .Height, relatedBy: .Equal, + toItem: nil, attribute: .NotAnAttribute, + multiplier: 1, constant: self.image!.size.height)) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("This class does not support NSCoding.") + } +} diff --git a/Example/TransparentCoachMarkBodyView.swift b/Example/TransparentCoachMarkBodyView.swift new file mode 100644 index 00000000..840882e8 --- /dev/null +++ b/Example/TransparentCoachMarkBodyView.swift @@ -0,0 +1,77 @@ +// TransparentCoachMarkBodyView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Instructions + +// Transparent coach mark (text without background, cool arrow) +internal class TransparentCoachMarkBodyView : UIControl, CoachMarkBodyView { + // MARK: - Internal properties + var nextControl: UIControl? { + get { + return self + } + } + + weak var highlightArrowDelegate: CoachMarkBodyHighlightArrowDelegate? = nil + + var hintLabel = UITextView(); + + // MARK: - Initialization + override init (frame: CGRect) { + super.init(frame: frame) + + self.setupInnerViewHierarchy() + } + + convenience init() { + self.init(frame: CGRectZero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("This class does not support NSCoding.") + } + + // MARK: - Private methods + private func setupInnerViewHierarchy() { + self.translatesAutoresizingMaskIntoConstraints = false + + hintLabel.backgroundColor = UIColor.clearColor() + hintLabel.textColor = UIColor.whiteColor() + hintLabel.font = UIFont.systemFontOfSize(15.0); + hintLabel.scrollEnabled = false; + hintLabel.textAlignment = .Justified; + hintLabel.layoutManager.hyphenationFactor = 2.0; + hintLabel.editable = false; + + hintLabel.translatesAutoresizingMaskIntoConstraints = false; + hintLabel.userInteractionEnabled = false; + + self.addSubview(hintLabel); + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[hintLabel]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["hintLabel": hintLabel])) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[hintLabel]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["hintLabel": hintLabel])) + } +} \ No newline at end of file diff --git a/Example/ViewController.swift b/Example/ViewController.swift deleted file mode 100644 index fb0c23bc..00000000 --- a/Example/ViewController.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ViewController.swift -// Instructions Example -// -// Created by Frédéric Maquin on 26/09/15. -// Copyright © 2015 Ephread. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - -} - diff --git a/Instructions Example.xcodeproj/project.pbxproj b/Instructions Example.xcodeproj/project.pbxproj index 6fa31824..c6d9dc7a 100644 --- a/Instructions Example.xcodeproj/project.pbxproj +++ b/Instructions Example.xcodeproj/project.pbxproj @@ -7,22 +7,64 @@ objects = { /* Begin PBXBuildFile section */ + C636FFAA1BBBD99200EB243B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C636FFA91BBBD99200EB243B /* Assets.xcassets */; settings = {ASSET_TAGS = (); }; }; + C636FFB11BBC329600EB243B /* TransparentCoachMarkArrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636FFAF1BBC329600EB243B /* TransparentCoachMarkArrowView.swift */; settings = {ASSET_TAGS = (); }; }; + C636FFB21BBC329600EB243B /* TransparentCoachMarkBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636FFB01BBC329600EB243B /* TransparentCoachMarkBodyView.swift */; settings = {ASSET_TAGS = (); }; }; C64FB3351BB6CC180081E5B6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB32D1BB6CC180081E5B6 /* AppDelegate.swift */; settings = {ASSET_TAGS = (); }; }; - C64FB3361BB6CC180081E5B6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C64FB32E1BB6CC180081E5B6 /* Assets.xcassets */; settings = {ASSET_TAGS = (); }; }; C64FB3371BB6CC180081E5B6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C64FB32F1BB6CC180081E5B6 /* LaunchScreen.storyboard */; settings = {ASSET_TAGS = (); }; }; C64FB3381BB6CC180081E5B6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C64FB3311BB6CC180081E5B6 /* Main.storyboard */; settings = {ASSET_TAGS = (); }; }; - C64FB3391BB6CC180081E5B6 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C64FB3331BB6CC180081E5B6 /* Info.plist */; settings = {ASSET_TAGS = (); }; }; - C64FB33A1BB6CC180081E5B6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3341BB6CC180081E5B6 /* ViewController.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB33A1BB6CC180081E5B6 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3341BB6CC180081E5B6 /* ProfileViewController.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB3841BB98CE90081E5B6 /* CustomCoachMarkBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3831BB98CE90081E5B6 /* CustomCoachMarkBodyView.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB3861BB98EC20081E5B6 /* CustomCoachMarkArrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3851BB98EC20081E5B6 /* CustomCoachMarkArrowView.swift */; settings = {ASSET_TAGS = (); }; }; + C6CE542C1BBD435500154266 /* BlurringOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CE54291BBD435500154266 /* BlurringOverlayViewController.swift */; settings = {ASSET_TAGS = (); }; }; + C6CE542D1BBD435500154266 /* CustomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CE542A1BBD435500154266 /* CustomViewController.swift */; settings = {ASSET_TAGS = (); }; }; + C6CE542E1BBD435500154266 /* DefaultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CE542B1BBD435500154266 /* DefaultViewController.swift */; settings = {ASSET_TAGS = (); }; }; + C6CE54311BBD43CB00154266 /* DelegateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CE542F1BBD43CB00154266 /* DelegateViewController.swift */; settings = {ASSET_TAGS = (); }; }; + C6CE54321BBD43CB00154266 /* MixedCoachMarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CE54301BBD43CB00154266 /* MixedCoachMarksViewController.swift */; settings = {ASSET_TAGS = (); }; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + C64FB36E1BB75F4D0081E5B6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C64FB3691BB75F4D0081E5B6 /* Instructions.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = C64FB2E91BB6CA8A0081E5B6; + remoteInfo = Instructions; + }; + C64FB3701BB75F4D0081E5B6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C64FB3691BB75F4D0081E5B6 /* Instructions.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = C64FB2F31BB6CA8A0081E5B6; + remoteInfo = InstructionsTests; + }; + C64FB3761BB75F980081E5B6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C64FB3691BB75F4D0081E5B6 /* Instructions.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = C64FB2E81BB6CA8A0081E5B6; + remoteInfo = Instructions; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + C636FFA91BBBD99200EB243B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Example/Assets.xcassets; sourceTree = ""; }; + C636FFAF1BBC329600EB243B /* TransparentCoachMarkArrowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransparentCoachMarkArrowView.swift; sourceTree = ""; }; + C636FFB01BBC329600EB243B /* TransparentCoachMarkBodyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransparentCoachMarkBodyView.swift; sourceTree = ""; }; C64FB30C1BB6CB1C0081E5B6 /* Instructions Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Instructions Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; C64FB32D1BB6CC180081E5B6 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - C64FB32E1BB6CC180081E5B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C64FB3301BB6CC180081E5B6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; C64FB3321BB6CC180081E5B6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - C64FB3331BB6CC180081E5B6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C64FB3341BB6CC180081E5B6 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + C64FB3331BB6CC180081E5B6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Example/Info.plist; sourceTree = ""; }; + C64FB3341BB6CC180081E5B6 /* ProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; + C64FB3691BB75F4D0081E5B6 /* Instructions.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = Instructions.xcodeproj; sourceTree = ""; }; + C64FB3831BB98CE90081E5B6 /* CustomCoachMarkBodyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCoachMarkBodyView.swift; sourceTree = ""; }; + C64FB3851BB98EC20081E5B6 /* CustomCoachMarkArrowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCoachMarkArrowView.swift; sourceTree = ""; }; + C6CE54291BBD435500154266 /* BlurringOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurringOverlayViewController.swift; sourceTree = ""; }; + C6CE542A1BBD435500154266 /* CustomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomViewController.swift; sourceTree = ""; }; + C6CE542B1BBD435500154266 /* DefaultViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultViewController.swift; sourceTree = ""; }; + C6CE542F1BBD43CB00154266 /* DelegateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DelegateViewController.swift; sourceTree = ""; }; + C6CE54301BBD43CB00154266 /* MixedCoachMarksViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixedCoachMarksViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -36,11 +78,31 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + C636FFAD1BBBF68300EB243B /* Supporting Files */ = { + isa = PBXGroup; + children = ( + C636FFA91BBBD99200EB243B /* Assets.xcassets */, + C64FB3331BB6CC180081E5B6 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + C636FFAE1BBBF69B00EB243B /* Storyboards */ = { + isa = PBXGroup; + children = ( + C64FB32F1BB6CC180081E5B6 /* LaunchScreen.storyboard */, + C64FB3311BB6CC180081E5B6 /* Main.storyboard */, + ); + name = Storyboards; + sourceTree = ""; + }; C64FB3031BB6CB1C0081E5B6 = { isa = PBXGroup; children = ( C64FB32C1BB6CC180081E5B6 /* Source */, + C636FFAD1BBBF68300EB243B /* Supporting Files */, C64FB30D1BB6CB1C0081E5B6 /* Products */, + C64FB3691BB75F4D0081E5B6 /* Instructions.xcodeproj */, ); sourceTree = ""; }; @@ -56,16 +118,47 @@ isa = PBXGroup; children = ( C64FB32D1BB6CC180081E5B6 /* AppDelegate.swift */, - C64FB32E1BB6CC180081E5B6 /* Assets.xcassets */, - C64FB32F1BB6CC180081E5B6 /* LaunchScreen.storyboard */, - C64FB3311BB6CC180081E5B6 /* Main.storyboard */, - C64FB3331BB6CC180081E5B6 /* Info.plist */, - C64FB3341BB6CC180081E5B6 /* ViewController.swift */, + C636FFAE1BBBF69B00EB243B /* Storyboards */, + C6CE54261BBD42D300154266 /* View Controllers */, + C6CE54281BBD42E500154266 /* Custom Views */, ); name = Source; path = Example; sourceTree = ""; }; + C64FB36A1BB75F4D0081E5B6 /* Products */ = { + isa = PBXGroup; + children = ( + C64FB36F1BB75F4D0081E5B6 /* Instructions.framework */, + C64FB3711BB75F4D0081E5B6 /* InstructionsTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + C6CE54261BBD42D300154266 /* View Controllers */ = { + isa = PBXGroup; + children = ( + C64FB3341BB6CC180081E5B6 /* ProfileViewController.swift */, + C6CE54291BBD435500154266 /* BlurringOverlayViewController.swift */, + C6CE542A1BBD435500154266 /* CustomViewController.swift */, + C6CE542B1BBD435500154266 /* DefaultViewController.swift */, + C6CE542F1BBD43CB00154266 /* DelegateViewController.swift */, + C6CE54301BBD43CB00154266 /* MixedCoachMarksViewController.swift */, + ); + name = "View Controllers"; + sourceTree = ""; + }; + C6CE54281BBD42E500154266 /* Custom Views */ = { + isa = PBXGroup; + children = ( + C64FB3851BB98EC20081E5B6 /* CustomCoachMarkArrowView.swift */, + C64FB3831BB98CE90081E5B6 /* CustomCoachMarkBodyView.swift */, + C636FFAF1BBC329600EB243B /* TransparentCoachMarkArrowView.swift */, + C636FFB01BBC329600EB243B /* TransparentCoachMarkBodyView.swift */, + ); + name = "Custom Views"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -80,6 +173,7 @@ buildRules = ( ); dependencies = ( + C64FB3771BB75F980081E5B6 /* PBXTargetDependency */, ); name = "Instructions Example"; productName = "Instructions Example"; @@ -111,6 +205,12 @@ mainGroup = C64FB3031BB6CB1C0081E5B6; productRefGroup = C64FB30D1BB6CB1C0081E5B6 /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = C64FB36A1BB75F4D0081E5B6 /* Products */; + ProjectRef = C64FB3691BB75F4D0081E5B6 /* Instructions.xcodeproj */; + }, + ); projectRoot = ""; targets = ( C64FB30B1BB6CB1C0081E5B6 /* Instructions Example */, @@ -118,14 +218,30 @@ }; /* End PBXProject section */ +/* Begin PBXReferenceProxy section */ + C64FB36F1BB75F4D0081E5B6 /* Instructions.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Instructions.framework; + remoteRef = C64FB36E1BB75F4D0081E5B6 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C64FB3711BB75F4D0081E5B6 /* InstructionsTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = InstructionsTests.xctest; + remoteRef = C64FB3701BB75F4D0081E5B6 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + /* Begin PBXResourcesBuildPhase section */ C64FB30A1BB6CB1C0081E5B6 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C64FB3391BB6CC180081E5B6 /* Info.plist in Resources */, C64FB3381BB6CC180081E5B6 /* Main.storyboard in Resources */, - C64FB3361BB6CC180081E5B6 /* Assets.xcassets in Resources */, + C636FFAA1BBBD99200EB243B /* Assets.xcassets in Resources */, C64FB3371BB6CC180081E5B6 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -137,13 +253,30 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C64FB33A1BB6CC180081E5B6 /* ViewController.swift in Sources */, + C64FB3861BB98EC20081E5B6 /* CustomCoachMarkArrowView.swift in Sources */, + C64FB3841BB98CE90081E5B6 /* CustomCoachMarkBodyView.swift in Sources */, + C636FFB11BBC329600EB243B /* TransparentCoachMarkArrowView.swift in Sources */, + C6CE542C1BBD435500154266 /* BlurringOverlayViewController.swift in Sources */, + C64FB33A1BB6CC180081E5B6 /* ProfileViewController.swift in Sources */, + C6CE54311BBD43CB00154266 /* DelegateViewController.swift in Sources */, C64FB3351BB6CC180081E5B6 /* AppDelegate.swift in Sources */, + C6CE542E1BBD435500154266 /* DefaultViewController.swift in Sources */, + C636FFB21BBC329600EB243B /* TransparentCoachMarkBodyView.swift in Sources */, + C6CE54321BBD43CB00154266 /* MixedCoachMarksViewController.swift in Sources */, + C6CE542D1BBD435500154266 /* CustomViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + C64FB3771BB75F980081E5B6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Instructions; + targetProxy = C64FB3761BB75F980081E5B6 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ C64FB32F1BB6CC180081E5B6 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -251,7 +384,11 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - INFOPLIST_FILE = "Instructions Example/Info.plist"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/Debug-iphoneos", + ); + INFOPLIST_FILE = Example/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.ephread.Instructions-Example"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -262,7 +399,11 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - INFOPLIST_FILE = "Instructions Example/Info.plist"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/Debug-iphoneos", + ); + INFOPLIST_FILE = Example/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.ephread.Instructions-Example"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Instructions Example.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/Instructions Example.xcscheme b/Instructions Example.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/Instructions Example.xcscheme deleted file mode 100644 index e6da376b..00000000 --- a/Instructions Example.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/Instructions Example.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Instructions Example.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/xcschememanagement.plist b/Instructions Example.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 1b83d6cb..00000000 --- a/Instructions Example.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - SchemeUserState - - Instructions Example.xcscheme - - orderHint - 1 - - - SuppressBuildableAutocreation - - C64FB30B1BB6CB1C0081E5B6 - - primary - - - - - diff --git a/Instructions.xcodeproj/project.pbxproj b/Instructions.xcodeproj/project.pbxproj index d447811b..2e61f15c 100644 --- a/Instructions.xcodeproj/project.pbxproj +++ b/Instructions.xcodeproj/project.pbxproj @@ -7,11 +7,25 @@ objects = { /* Begin PBXBuildFile section */ + C61458B31BBB01DE006EB4F2 /* CoachMarkArrowOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61458B21BBB01DE006EB4F2 /* CoachMarkArrowOrientation.swift */; settings = {ASSET_TAGS = (); }; }; + C636FFB41BBC8E0000EB243B /* OverlayViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636FFB31BBC8E0000EB243B /* OverlayViewDelegate.swift */; settings = {ASSET_TAGS = (); }; }; + C636FFB91BBC8F3100EB243B /* CoachMarkBodyDefaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636FFB81BBC8F3100EB243B /* CoachMarkBodyDefaultView.swift */; settings = {ASSET_TAGS = (); }; }; + C636FFBB1BBC8FC300EB243B /* CoachMarkArrowDefaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636FFBA1BBC8FC300EB243B /* CoachMarkArrowDefaultView.swift */; settings = {ASSET_TAGS = (); }; }; C64FB2F41BB6CA8A0081E5B6 /* Instructions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C64FB2E91BB6CA8A0081E5B6 /* Instructions.framework */; settings = {ASSET_TAGS = (); }; }; - C64FB3241BB6CB5C0081E5B6 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C64FB3221BB6CB5C0081E5B6 /* Info.plist */; settings = {ASSET_TAGS = (); }; }; - C64FB3251BB6CB5C0081E5B6 /* Instructions.h in Headers */ = {isa = PBXBuildFile; fileRef = C64FB3231BB6CB5C0081E5B6 /* Instructions.h */; settings = {ASSET_TAGS = (); }; }; + C64FB3251BB6CB5C0081E5B6 /* Instructions.h in Headers */ = {isa = PBXBuildFile; fileRef = C64FB3231BB6CB5C0081E5B6 /* Instructions.h */; settings = {ATTRIBUTES = (Public, ); }; }; C64FB3291BB6CB680081E5B6 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C64FB3271BB6CB680081E5B6 /* Info.plist */; settings = {ASSET_TAGS = (); }; }; C64FB32A1BB6CB680081E5B6 /* InstructionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3281BB6CB680081E5B6 /* InstructionsTests.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB3431BB6D81C0081E5B6 /* CoachMarksController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3411BB6D1020081E5B6 /* CoachMarksController.swift */; }; + C64FB3441BB6D81C0081E5B6 /* CoachMarksControllerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB33D1BB6CE2C0081E5B6 /* CoachMarksControllerDataSource.swift */; }; + C64FB3451BB6D81C0081E5B6 /* CoachMarksControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB33F1BB6D0160081E5B6 /* CoachMarksControllerDelegate.swift */; }; + C64FB3471BB6D91E0081E5B6 /* CoachMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3461BB6D91E0081E5B6 /* CoachMark.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB3491BB6DC3B0081E5B6 /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3481BB6DC3B0081E5B6 /* OverlayView.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB34B1BB6DD1B0081E5B6 /* CoachMarkBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB34A1BB6DD1B0081E5B6 /* CoachMarkBodyView.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB34D1BB6DEDC0081E5B6 /* CoachMarkArrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB34C1BB6DEDC0081E5B6 /* CoachMarkArrowView.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB3521BB6FA250081E5B6 /* CoachMarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3511BB6FA250081E5B6 /* CoachMarkView.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB3541BB747590081E5B6 /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB3531BB747590081E5B6 /* Instructions.swift */; settings = {ASSET_TAGS = (); }; }; + C64FB37A1BB7DE930081E5B6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C64FB3791BB7DE930081E5B6 /* Images.xcassets */; settings = {ASSET_TAGS = (); }; }; + C64FB37E1BB828E50081E5B6 /* CoachMarkBodyHighlightArrowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64FB37D1BB828E50081E5B6 /* CoachMarkBodyHighlightArrowDelegate.swift */; settings = {ASSET_TAGS = (); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -25,12 +39,27 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + C61458B21BBB01DE006EB4F2 /* CoachMarkArrowOrientation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoachMarkArrowOrientation.swift; sourceTree = ""; }; + C636FFB31BBC8E0000EB243B /* OverlayViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayViewDelegate.swift; sourceTree = ""; }; + C636FFB81BBC8F3100EB243B /* CoachMarkBodyDefaultView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoachMarkBodyDefaultView.swift; sourceTree = ""; }; + C636FFBA1BBC8FC300EB243B /* CoachMarkArrowDefaultView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoachMarkArrowDefaultView.swift; sourceTree = ""; }; C64FB2E91BB6CA8A0081E5B6 /* Instructions.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Instructions.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C64FB2F31BB6CA8A0081E5B6 /* InstructionsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InstructionsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C64FB3221BB6CB5C0081E5B6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C64FB3231BB6CB5C0081E5B6 /* Instructions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Instructions.h; sourceTree = ""; }; C64FB3271BB6CB680081E5B6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C64FB3281BB6CB680081E5B6 /* InstructionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTests.swift; sourceTree = ""; }; + C64FB33D1BB6CE2C0081E5B6 /* CoachMarksControllerDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CoachMarksControllerDataSource.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + C64FB33F1BB6D0160081E5B6 /* CoachMarksControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoachMarksControllerDelegate.swift; sourceTree = ""; }; + C64FB3411BB6D1020081E5B6 /* CoachMarksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CoachMarksController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + C64FB3461BB6D91E0081E5B6 /* CoachMark.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoachMark.swift; sourceTree = ""; }; + C64FB3481BB6DC3B0081E5B6 /* OverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = ""; }; + C64FB34A1BB6DD1B0081E5B6 /* CoachMarkBodyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoachMarkBodyView.swift; sourceTree = ""; }; + C64FB34C1BB6DEDC0081E5B6 /* CoachMarkArrowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoachMarkArrowView.swift; sourceTree = ""; }; + C64FB3511BB6FA250081E5B6 /* CoachMarkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoachMarkView.swift; sourceTree = ""; }; + C64FB3531BB747590081E5B6 /* Instructions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = ""; }; + C64FB3791BB7DE930081E5B6 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + C64FB37D1BB828E50081E5B6 /* CoachMarkBodyHighlightArrowDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoachMarkBodyHighlightArrowDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -52,6 +81,53 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + C61458B41BBB01F2006EB4F2 /* Enums */ = { + isa = PBXGroup; + children = ( + C61458B21BBB01DE006EB4F2 /* CoachMarkArrowOrientation.swift */, + ); + name = Enums; + sourceTree = ""; + }; + C61458B51BBB0201006EB4F2 /* Extra */ = { + isa = PBXGroup; + children = ( + C636FFB71BBC8EF800EB243B /* Default Views */, + C61458B41BBB01F2006EB4F2 /* Enums */, + C64FB33C1BB6CDBB0081E5B6 /* Protocols */, + ); + name = Extra; + sourceTree = ""; + }; + C636FFB51BBC8E9800EB243B /* Internal */ = { + isa = PBXGroup; + children = ( + C636FFB31BBC8E0000EB243B /* OverlayViewDelegate.swift */, + ); + name = Internal; + sourceTree = ""; + }; + C636FFB61BBC8ED600EB243B /* Public */ = { + isa = PBXGroup; + children = ( + C64FB34A1BB6DD1B0081E5B6 /* CoachMarkBodyView.swift */, + C64FB34C1BB6DEDC0081E5B6 /* CoachMarkArrowView.swift */, + C64FB37D1BB828E50081E5B6 /* CoachMarkBodyHighlightArrowDelegate.swift */, + C64FB33D1BB6CE2C0081E5B6 /* CoachMarksControllerDataSource.swift */, + C64FB33F1BB6D0160081E5B6 /* CoachMarksControllerDelegate.swift */, + ); + name = Public; + sourceTree = ""; + }; + C636FFB71BBC8EF800EB243B /* Default Views */ = { + isa = PBXGroup; + children = ( + C636FFB81BBC8F3100EB243B /* CoachMarkBodyDefaultView.swift */, + C636FFBA1BBC8FC300EB243B /* CoachMarkArrowDefaultView.swift */, + ); + name = "Default Views"; + sourceTree = ""; + }; C64FB2DF1BB6CA8A0081E5B6 = { isa = PBXGroup; children = ( @@ -73,6 +149,12 @@ C64FB3211BB6CB5C0081E5B6 /* Source */ = { isa = PBXGroup; children = ( + C64FB3531BB747590081E5B6 /* Instructions.swift */, + C64FB3411BB6D1020081E5B6 /* CoachMarksController.swift */, + C64FB3461BB6D91E0081E5B6 /* CoachMark.swift */, + C64FB3481BB6DC3B0081E5B6 /* OverlayView.swift */, + C64FB3511BB6FA250081E5B6 /* CoachMarkView.swift */, + C61458B51BBB0201006EB4F2 /* Extra */, C64FB33B1BB6CC5B0081E5B6 /* Supporting Files */, ); path = Source; @@ -92,10 +174,20 @@ children = ( C64FB3221BB6CB5C0081E5B6 /* Info.plist */, C64FB3231BB6CB5C0081E5B6 /* Instructions.h */, + C64FB3791BB7DE930081E5B6 /* Images.xcassets */, ); name = "Supporting Files"; sourceTree = ""; }; + C64FB33C1BB6CDBB0081E5B6 /* Protocols */ = { + isa = PBXGroup; + children = ( + C636FFB61BBC8ED600EB243B /* Public */, + C636FFB51BBC8E9800EB243B /* Internal */, + ); + name = Protocols; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -186,7 +278,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C64FB3241BB6CB5C0081E5B6 /* Info.plist in Resources */, + C64FB37A1BB7DE930081E5B6 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -205,6 +297,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C636FFBB1BBC8FC300EB243B /* CoachMarkArrowDefaultView.swift in Sources */, + C64FB3431BB6D81C0081E5B6 /* CoachMarksController.swift in Sources */, + C64FB3521BB6FA250081E5B6 /* CoachMarkView.swift in Sources */, + C64FB3441BB6D81C0081E5B6 /* CoachMarksControllerDataSource.swift in Sources */, + C61458B31BBB01DE006EB4F2 /* CoachMarkArrowOrientation.swift in Sources */, + C636FFB91BBC8F3100EB243B /* CoachMarkBodyDefaultView.swift in Sources */, + C64FB3491BB6DC3B0081E5B6 /* OverlayView.swift in Sources */, + C64FB37E1BB828E50081E5B6 /* CoachMarkBodyHighlightArrowDelegate.swift in Sources */, + C64FB3451BB6D81C0081E5B6 /* CoachMarksControllerDelegate.swift in Sources */, + C64FB3541BB747590081E5B6 /* Instructions.swift in Sources */, + C636FFB41BBC8E0000EB243B /* OverlayViewDelegate.swift in Sources */, + C64FB34D1BB6DEDC0081E5B6 /* CoachMarkArrowView.swift in Sources */, + C64FB34B1BB6DD1B0081E5B6 /* CoachMarkBodyView.swift in Sources */, + C64FB3471BB6D91E0081E5B6 /* CoachMark.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -323,7 +429,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Instructions/Info.plist; + INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ephread.Instructions; @@ -339,7 +445,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Instructions/Info.plist; + INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ephread.Instructions; diff --git a/Instructions.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/Instructions.xcscheme b/Instructions.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/Instructions.xcscheme deleted file mode 100644 index 9ec784b1..00000000 --- a/Instructions.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/Instructions.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Instructions.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/xcschememanagement.plist b/Instructions.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index f5af7612..00000000 --- a/Instructions.xcodeproj/xcuserdata/frederic.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,27 +0,0 @@ - - - - - SchemeUserState - - Instructions.xcscheme - - orderHint - 0 - - - SuppressBuildableAutocreation - - C64FB2E81BB6CA8A0081E5B6 - - primary - - - C64FB2F21BB6CA8A0081E5B6 - - primary - - - - - diff --git a/Instructions.xcworkspace/xcuserdata/frederic.xcuserdatad/UserInterfaceState.xcuserstate b/Instructions.xcworkspace/xcuserdata/frederic.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 7db74ed0..00000000 Binary files a/Instructions.xcworkspace/xcuserdata/frederic.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ee39a553 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Frédéric Maquin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..0de2101c --- /dev/null +++ b/README.md @@ -0,0 +1,244 @@ +# ![Instructions](http://i.imgur.com/927crlD.png) + +Add customizable coach marks into you iOS project. Instructions will makes your coach-mark-maker life easier, I promise. Available for both iPhone and iPad. + +## Overview +![Instructions Demo](http://i.imgur.com/JUlQH9F.gif) + +⚠️ **This project is not yet production ready, but it will be soon. Until Instructions reaches 1.0.0, the API is subject to change. Please see the Features section for more informations about the roadmap.** + +## Features +- [x] Customizable views +- [x] Customizable positions +- [x] Customizable highlight system +- [ ] Full right-to-left support • **will make Instructions production-ready!** +- [ ] Size changes support (orientation and multi-tasking) • **will make Instructions production-ready!** +- [ ] Full support of UIVisualEffectView blur in overlay +- [ ] Objective-C bridging +- [ ] Animations on coach marks themselves support + +## Requirements +- Xcode 7 / Swift 2 +- iOS 8.0+ for the library itself +- iOS 9.0+ for the example application (couldn't resist using `UIStackView`) + +## Installation + +### Cocoapods +TBA + +### Carthage +TBA + +### Manually +If you rather stay away from both Cocoapods and Carthage, you can also install Instructions manually, with the cost of managing updates yourself. + +#### Embedded Framework +1. Drag the Instructions.xcodeproj into the project navigator of your application's Xcode project. +2. Still in the project navigator, select your application project. The target configuration panel should show up. +3. Select the appropriate target and in the "General" panel, scroll down to the section named "Embedded Binaries". +4. Click on the + button and select the "Instructions.framework" under the "Product" directory. + +## Usage + +### Getting started +Open up the controller for which you wish to display coach marks and instanciate a new `CoachMarksViewController`. You should also provide a `datasource`, which is an object conforming to the `CoachMarksControllerDataSource` protocol. + +```swift +class DefaultViewController: UIViewController, CoachMarksControllerDataSource, CoachMarksControllerDelegate { + let coachMarksController = CoachMarksController() + + override func viewDidLoad() { + super.viewDidLoad() + + self.coachMarksController.datasource = self + } +} +``` + +#### Data Source +`CoachMarksControllerDataSource` declares three methods, all mandatory. + +The first one asks for the number of coach marks to display. Let's pretend that you want to display only one coach mark. Note that the `CoachMarksController` requesting the information is supplied, allowing you to supply data for mutiple `CoachMarksController`, within a single datasource. + +```swift +override func numberOfCoachMarksForCoachMarksController(_: CoachMarksController) +-> Int { + return 1 +} +``` + +The second one asks for metadata. This allows you to customize how a coach mark will position and appear, but won't let you define its look (more on this later). Metadata are packaged in a struct named `CoachMark`. Note the parameter `coachMarksForIndex`, it gives you the coach mark logical position, much like and `IndexPath` would do. `coachMarksController` provides you with an easy way to create a default `CoachMark` object, from a given view. + +```swift +let pointOfInterest = UIView() + +override func coachMarksController(coachMarksController: CoachMarksController, coachMarksForIndex: Int) +-> CoachMark { + return coachMarksController.coachMarkForView(self.pointOfInterest) +} +``` + +The third one supplies two views (much like `cellForRowAtIndexPath`) in the form a Tuple. The _body_ view is mandatory, as it's the core of the coach mark. The _arrow_ view is optional. + +But for now, lets just return the default views provided by Instructions. + +```swift +override func coachMarksController(_: CoachMarksController, coachMarkViewsForIndex: Int, coachMark: CoachMark) +-> (bodyView: CoachMarkBodyView, arrowView: CoachMarkArrowView?) { + let coachViews = coachMarksController.defaultCoachViewsWithArrow(true, arrowOrientation: coachMark.arrowOrientation) + + coachViews.bodyView.hintLabel.text = "Hello! I'm a Coach Mark!" + coachViews.bodyView.nextLabel.text = "Got it!" + + return (bodyView: coachViews.bodyView, arrowView: coachViews.arrowView) +} +``` + +#### Starting the coach marks flow +Once the `datasource` is set up, you can start displaying the coach marks. You can supply `self` to `startOn`, but if you want the background overlay to cover the navigation/tab bar, you will most likely need to supply the parent `UINavigationController` or `UITabBarViewController`. + +```swift +override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + + self.coachMarksController.startOn(self.navigationController!) +} +``` + +You're all set. For more examples you can check the `Examples/` directory provided with the library. + +### Advanced Usage + +#### Customizing general properties +You can customized the background color of the overlay using this property: + +- `overlayBackgroundColor` + +You can also make the overlay blur the content sitting behind it. Setting this property to anything else than `nil` will disable the `overlayBackgroundColor`: + +- `overlayBlurEffectStyle: UIBlurEffectStyle?` + +Last, you can make the overlay tappable. A tap on the overlay will hide the current coach mark and display the next one. + +- `allowOverlayTap: Bool` + +#### Providing a custom cutout path +If you dislike how the default cutout path looks like, you can customize it by providing a block to `coachMarkForView`. The cutout path will automatically be stored in the `cutoutPath` property of the returning `CoachMark` object: + +```swift +var coachMark = coachMarksController.coachMarkForView(customView) { +(frame: CGRect) -> UIBezierPath in + // This will create an oval cutout a bit larger than the view. + return UIBezierPath(ovalInRect: CGRectInset(frame, -4, -4)) +} +``` + +`frame` will be the frame of `customView` converted in the `coachMarksController.view` referential, so don't have to worry about making sure the coordinates are in the appropriate referential. You can provide any kind of shape, from a simple rectangle to a complex star. + +#### Providing custom views +You can (and you should) provide custom views. A coach mark is composed of two views, a _body_ view and an _arrow_ view. Note that the term _arrow_ might be misleading. It doesn't have to be an actual arrow, it can be anything you want. + +A _body_ view must conform to the `CoachMarkBodyView` protocol. An _arrow_ view must conform to the `CoachMarkArrowView` protocol. Both of them must also be subclasses of `UIView`. + +Returning a `CoachMarkBodyView` view is mandatory, while returning a `CoachMarkArrowView` is optional. + +##### CoachMarkBodyView Protocol ##### +This protocol defines two properties. + +- `nextControl: UIControl? { get }` you must implement a getter method for this property in your view, this will let the `CoachMarkController` know which control should be tapped, to display the next coach mark. Note that it doesn't have to be a subview, you can return the view itself. + +- `highlightArrowDelegate: CoachMarkBodyHighlightArrowDelegate?` in case the view itself is the control receiving taps, you might want to forward its highlight state to the _arrow_ view (so they can look as if they are the same component). The `CoachMarkController` will automatically set an appropriate delegate to this property. You'll then be able to do this: + +```swift +override var highlighted: Bool { + didSet { + self.highlightArrowDelegate?.highlightArrow(self.highlighted) + } +} +``` + +##### Taking orientation into account ##### +Remember the following method, from the datasource? + +```swift +func coachMarksController(coachMarkController: CoachMarksController, coachMarkViewsForIndex: Int, coachMark: CoachMark) { + let coachViews = coachMarksController.defaultCoachViewsWithArrow(true, arrowOrientation: coachMark.arrowOrientation) +} +``` + +When providing a customized view, you need to provide an _arrow_ view with the approriate orientation (i. e. in the case of an actual arrow, pointing upward or downward). The `CoachMarkController` will tell you which orientation it expects, through the following property: `CoachMark.arrowOrientation`. + +Browse the `Example/` directory for more details. + +#### Customizing how the coach mark will show +You can customize the following properties: + +- `animationDuration: NSTimeInterval`: the time it will take for a coach mark to appear or disappear on the screen. + +- `gapBetweenBodyAndArrow: CGFloat`: the vertical gap between the _body_ and the _arrow_ in a given coach mark. + +- `pointOfInterest: CGPoint?`: the point toward which the arrow will face. At the moment, it's only used to shift the arrow horizontally and make it sits above or below the point of interest. + +- `gapBetweenCoachMarkAndCutoutPath: CGFloat`: the gap between the coach mark and the cutout path. + +- `maxWidth: CGFloat`: the maximum width a coach mark can take. You don't want your coach marks to be too wide, especially on iPads. + +- `horizontalMargin: CGFloat` is the margin (both leading and trailing) between the edges of the overlay view and the coach mark. Note that if the max width of your coach mark is less than the width of the overlay view, you view will either stack on the left or on the right, leaving space on the other side. + +#### Using a delegate +The `CoachMarkManager` will notify the delegate on three occasions. All those methods are optionals. + +First, when a coach mark will show. You might want to change something about the view. For that reason, the `CoachMark` metadata structure is passed as an `inout` object, so you can update it with new parameters. + +```swift +func coachMarksController(_: CoachMarksController, inout coachMarkWillShow: CoachMark, forIndex: Int) { + +} +``` + +Second, when a coach mark disappears. + +```swift +func coachMarksController(_: CoachMarksController, coachMarkWillDisappear: CoachMark, forIndex: Int) { + +} +``` +Third, when all coach marks have been displayed. + +```swift +func didFinishShowingFromCoachMarksController(_: CoachMarksController) { + +} +``` + +##### Performing animations before showing coach marks ##### +You can perform animation on views, before or after showing a given coach mark. +For instance, you might want to collapse a table view and show only its header, before referring to those headers with a coach mark. Instructions offers a simple way to insert your own animations into the flow. + +For instance, let's say you want to perform an animation _before_ a coach mark shows. +You'll implement some logic into the `coachMarkWillShow` delegate method. +To ensure you don't have to hack something up and turn asynchronous animation blocks into synchronous ones, you can pause the flow, perform the animation and then start the flow again. This will ensure your UI never get stalled. + +```swift +func coachMarksController(_: CoachMarksController, inout coachMarkWillShow: CoachMark, forIndex: Int) { + // Pause to be able to play the animation and then show the coach mark. + coachMarksController.pause() + + // Run the animation + UIView.animateWithDuration(1, animations: { () -> Void in + … + }, completion: { (finished: Bool) -> Void in + // Once the animation is completed, we update the coach mark, + // and start the display again. Since inout parameters cannot be + // captured by the closure, you can use the following method to update + // the coachmark. It will only work if you paused the flow. + coachMarksController.updateCurrentCoachMarkForView(myView) + coachMarksController.play() + }) +} +``` + +## License + +Instructions is released under the MIT license. See LICENSE for details. \ No newline at end of file diff --git a/Source/CoachMark.swift b/Source/CoachMark.swift new file mode 100644 index 00000000..fd0f5e19 --- /dev/null +++ b/Source/CoachMark.swift @@ -0,0 +1,93 @@ +// CoachMark.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +/// This structure handle the parametrization of a given coach mark. +/// It doesn't provide any clue about the way it will look, however. +public struct CoachMark { + //MARK: - Public properties + + /// Change this value to change the duration of the fade. + public var animationDuration = kCoachMarkFadeAnimationDuration + + /// The path to cut in the overlay, so the point of interest will be visible. + public var cutoutPath: UIBezierPath? + + /// The vertical offset for the arrow (in rare cases, the arrow might need to overlap with the coach mark body). + public var gapBetweenBodyAndArrow: CGFloat = 2.0 + + /// The orientation of the arrow, around the body of the coach mark (top or bottom) + public private(set) var arrowOrientation: CoachMarkArrowOrientation? + + /// The "point of interest" toward which the arrow will point. + /// At the moment, it's only used to shift the arrow horizontally + /// and make it sits above/below the point of interest. + public var pointOfInterest: CGPoint? + + /// Offset between the coach mark and the cutout path. + public var gapBetweenCoachMarkAndCutoutPath: CGFloat = 2.0 + + /// Maximum width for a coach mark. + public var maxWidth: CGFloat = 350 + + /// Trailing and leading margin of the coach mark. + public var horizontalMargin: CGFloat = 20 + + //MARK: - Initialization + /// Allocate and initiliaze a Coach mark with default values + public init () { + + } + + //MARK: - Internal Methods + /// Compute the orientation of the arrow, given the frame in which the coach mark will be displayed. + /// + /// - Parameter frame: the frame in which compute the orientation (likely to match the overlay's frame) + internal mutating func computeOrientationInFrame(frame: CGRect) { + /// No cutout path means no arrow. That way, no orientation computation is needed. + guard let cutoutPath = self.cutoutPath else { + self.arrowOrientation = nil + return + } + + if cutoutPath.bounds.origin.y > frame.size.height / 2 { + self.arrowOrientation = .Bottom + } else { + self.arrowOrientation = .Top + } + } + + /// Compute the orientation of the arrow, given the frame in which the coach mark will be displayed. + internal mutating func computePointOfInterestInFrame() { + /// If the value is already set, don't do anything. + if self.pointOfInterest != nil { return } + + /// No cutout path means no point of interest. That way, no orientation computation is needed. + guard let cutoutPath = self.cutoutPath else { return } + + let x = cutoutPath.bounds.origin.x + cutoutPath.bounds.width / 2 + let y = cutoutPath.bounds.origin.y + cutoutPath.bounds.height / 2 + + self.pointOfInterest = CGPoint(x: x, y: y) + } +} \ No newline at end of file diff --git a/Source/CoachMarkArrowDefaultView.swift b/Source/CoachMarkArrowDefaultView.swift new file mode 100644 index 00000000..5eb7f772 --- /dev/null +++ b/Source/CoachMarkArrowDefaultView.swift @@ -0,0 +1,50 @@ +// CoachMarkArrowDefaultView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +/// A concrete implementation of the coach mark arrow view and the +/// default one provided by the library. +public class CoachMarkArrowDefaultView : UIImageView, CoachMarkArrowView { + //MARK: - Initialization + public init(orientation: CoachMarkArrowOrientation) { + if orientation == .Top { + super.init(image: UIImage(named: "arrow-top", inBundle: NSBundle(identifier: "com.ephread.Instructions"), compatibleWithTraitCollection: nil), highlightedImage: UIImage(named: "arrow-top-highlighted", inBundle: NSBundle(identifier: "com.ephread.Instructions"), compatibleWithTraitCollection: nil)!) + } else { + super.init(image: UIImage(named: "arrow-bottom", inBundle: NSBundle(identifier: "com.ephread.Instructions"), compatibleWithTraitCollection: nil), highlightedImage: UIImage(named: "arrow-bottom-highlighted", inBundle: NSBundle(identifier: "com.ephread.Instructions"), compatibleWithTraitCollection: nil)!) + } + + self.translatesAutoresizingMaskIntoConstraints = false + + self.addConstraint(NSLayoutConstraint(item: self, attribute: .Width, relatedBy: .Equal, + toItem: nil, attribute: .NotAnAttribute, + multiplier: 1, constant: self.image!.size.width)) + + self.addConstraint(NSLayoutConstraint(item: self, attribute: .Height, relatedBy: .Equal, + toItem: nil, attribute: .NotAnAttribute, + multiplier: 1, constant: self.image!.size.height)) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("This class does not support NSCoding.") + } +} \ No newline at end of file diff --git a/Source/CoachMarkArrowOrientation.swift b/Source/CoachMarkArrowOrientation.swift new file mode 100644 index 00000000..f1ed63c7 --- /dev/null +++ b/Source/CoachMarkArrowOrientation.swift @@ -0,0 +1,31 @@ +// CoachMarkArrowOrientation.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Available orientations for the arrow. +/// An arrow can either sit at the top and point upward (.Top) or +/// sit at the bottom and point downward. (.Bottom) +public enum CoachMarkArrowOrientation { + case Top + case Bottom +} \ No newline at end of file diff --git a/Source/CoachMarkArrowView.swift b/Source/CoachMarkArrowView.swift new file mode 100644 index 00000000..39a96a56 --- /dev/null +++ b/Source/CoachMarkArrowView.swift @@ -0,0 +1,31 @@ +// CoachMarkArrowView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// A protocol to which all the "arrow views" of a coach mark must conform. +public protocol CoachMarkArrowView : class { + /// A method to change the arrow highlighted state. + /// If you feel the arrow should mirror the state of the "body view", + /// You will most likely change the background color of the view here. + var highlighted: Bool { set get } +} diff --git a/Source/CoachMarkBodyDefaultView.swift b/Source/CoachMarkBodyDefaultView.swift new file mode 100644 index 00000000..eb97cba3 --- /dev/null +++ b/Source/CoachMarkBodyDefaultView.swift @@ -0,0 +1,130 @@ +// CoachMarkBodyDefaultView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +/// A concrete implementation of the coach mark body view and the +/// default one provided by the library. +public class CoachMarkBodyDefaultView : UIControl, CoachMarkBodyView { + //MARK: - Public properties + public var nextControl: UIControl? { + get { + return self + } + } + + public weak var highlightArrowDelegate: CoachMarkBodyHighlightArrowDelegate? + + override public var highlighted: Bool { + didSet { + if (self.highlighted) { + self.backgroundImageView.image = highlightedBackgroundImage + } else { + self.backgroundImageView.image = backgroundImage + } + + self.highlightArrowDelegate?.highlightArrow(self.highlighted) + } + } + + public var nextLabel = UILabel(); + public var hintLabel = UITextView(); + public var separator = UIView(); + + //MARK: - Private properties + private let backgroundImage = UIImage(named: "background", inBundle: NSBundle(identifier: "com.ephread.Instructions"), compatibleWithTraitCollection: nil) + + private let highlightedBackgroundImage = UIImage(named: "background-highlighted", inBundle: NSBundle(identifier: "com.ephread.Instructions"), compatibleWithTraitCollection: nil) + + private let backgroundImageView: UIImageView + + //MARK: - Initialization + override public init (frame: CGRect) { + self.backgroundImageView = UIImageView(image: self.backgroundImage) + + super.init(frame: frame) + + self.setupInnerViewHierarchy() + } + + convenience public init() { + self.init(frame: CGRectZero) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("This class does not support NSCoding.") + } + + //MARK: - Private properties + private func setupInnerViewHierarchy() { + self.translatesAutoresizingMaskIntoConstraints = false + self.backgroundImageView.translatesAutoresizingMaskIntoConstraints = false + self.backgroundImageView.userInteractionEnabled = false + + self.addSubview(self.backgroundImageView) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[backgroundImageView]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["backgroundImageView": self.backgroundImageView])) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[backgroundImageView]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["backgroundImageView": self.backgroundImageView])) + + + hintLabel.backgroundColor = UIColor.clearColor() + hintLabel.textColor = UIColor.darkGrayColor() + hintLabel.font = UIFont.systemFontOfSize(15.0); + hintLabel.scrollEnabled = false; + hintLabel.textAlignment = .Justified; + hintLabel.layoutManager.hyphenationFactor = 2.0; + hintLabel.editable = false; + + nextLabel.textColor = UIColor.darkGrayColor() + nextLabel.font = UIFont.systemFontOfSize(17.0); + nextLabel.textAlignment = .Center; + + separator.backgroundColor = UIColor.grayColor(); + + nextLabel.translatesAutoresizingMaskIntoConstraints = false; + hintLabel.translatesAutoresizingMaskIntoConstraints = false; + separator.translatesAutoresizingMaskIntoConstraints = false; + + nextLabel.userInteractionEnabled = false; + hintLabel.userInteractionEnabled = false; + separator.userInteractionEnabled = false; + + self.addSubview(nextLabel); + self.addSubview(hintLabel); + self.addSubview(separator); + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[nextLabel]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["nextLabel": nextLabel])) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(5)-[hintLabel]-(5)-|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["hintLabel": hintLabel])) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(15)-[separator]-(15)-|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["separator": separator])) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(10)-[hintLabel]-(10)-[separator(==1)][nextLabel(==55)]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["hintLabel": hintLabel, "separator": separator, "nextLabel": nextLabel])) + } +} diff --git a/Source/CoachMarkBodyHighlightArrowDelegate.swift b/Source/CoachMarkBodyHighlightArrowDelegate.swift new file mode 100644 index 00000000..343a9a86 --- /dev/null +++ b/Source/CoachMarkBodyHighlightArrowDelegate.swift @@ -0,0 +1,35 @@ +// CoachMarkBodyHighlightArrowDelegate.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Delegate the hilight mecanism of the arrow. This protocol is +/// useful in case the whole body itself is the active control and +/// we want the arrow to looks like it is part of this control. +public protocol CoachMarkBodyHighlightArrowDelegate : class { + + /// Set wethe ror not the arrow should get in its + /// highlighted state. + /// + /// - Parameters highlighted: `true` if the arrow should be highlighted, `false` otherwise. + func highlightArrow(highlighted: Bool); +} \ No newline at end of file diff --git a/Source/CoachMarkBodyView.swift b/Source/CoachMarkBodyView.swift new file mode 100644 index 00000000..4f4d944c --- /dev/null +++ b/Source/CoachMarkBodyView.swift @@ -0,0 +1,38 @@ +// CoachMarkBodyView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// A protocol to which all the "body views" of a coach mark must conform. +public protocol CoachMarkBodyView : class { + /// The control that will trigger the change between the current coach mark + /// and the next one. + var nextControl: UIControl? { get } + + /// A delegate to call, when the arrow view to mirror the current highlight + /// state of the body view. This is useful in case the entier view is actually a `UIControl`. + /// + /// The `CoachMarkView`, of which the current view must be + /// part, will automatically set itself as the delegate and will take care + /// of fowarding the state to the arrow view. + weak var highlightArrowDelegate: CoachMarkBodyHighlightArrowDelegate? { get set } +} diff --git a/Source/CoachMarkView.swift b/Source/CoachMarkView.swift new file mode 100644 index 00000000..dc127987 --- /dev/null +++ b/Source/CoachMarkView.swift @@ -0,0 +1,228 @@ +// CoachMarkView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +/// The actual coach mark that will be displayed. +/// +/// Note: This class is final for two reasons: +/// 1. It doesn't implement properly all the UIView initializers +/// 2. It is not suppoed to be subclassed at the moment, as it only acts as +/// container for body and arrow views. +final internal class CoachMarkView : UIView, CoachMarkBodyHighlightArrowDelegate { + + //MARK: - Internal sub elements + + /// Define the position of the arrow. + enum ArrowPosition { + case Leading + case Center + case Trailing + } + + //MARK: - Internal properties + + /// The body of the coach mark (likely to contain some text). + let bodyView: CoachMarkBodyView + + /// The arrow view, note that the arrow view is not mandatory. + private(set) var arrowView: CoachMarkArrowView? + + /// The arrow orientation (where it will sit relative to the body view, i.e. + /// above or below.) + private(set) var arrowOrientation: CoachMarkArrowOrientation? + + /// The offset (in case the arrow is required to overlap the body) + var arrowOffset: CGFloat = 0.0 + + /// The control used to get to the next coach mark. + var nextControl: UIControl? { + get { + return bodyView.nextControl + } + } + + //MARK: - Private properties + + /// The horizontal position of the arrow, likely to be at the center of the + /// cutout path. + var arrowXpositionConstraint: NSLayoutConstraint? + + /// The constraint making the arrow stick to its parent. + var arrowStickToParent: NSLayoutConstraint? + + /// The constraint making the arrow stick to its body. + var arrowStickToBodyConstraint: NSLayoutConstraint? + + /// The constraint making the body stick to its parent. + var bodyStickToParent: NSLayoutConstraint? + + //MARK: - Initialization + + /// Allocate and initliaze the coach mark view, with the given subviews. + /// + /// - Parameter bodyView: the mandatory body view + /// - Parameter arrowView: the optional arrow view + /// - Parameter arrowOrientation: the arrow orientation, either .Top or .Bottom + /// - Parameter arrowOffset: the arrow offset (in case the arrow is required to overlap the body) - a positive number will make the arrow overlap. + init(bodyView: CoachMarkBodyView, arrowView: CoachMarkArrowView? = nil, + arrowOrientation: CoachMarkArrowOrientation? = nil, arrowOffset: CGFloat? = 0.0) { + + // Due to the fact Swift 2 compiler doesn't let us enforce type check of + // an object being a class conforming to a given protocol, we are checking the + // type of body and arrow views at runtime. This isn't very nice, but I haven't found any + // better way to enforce that they both are subclasses of `UIView` and conform to the `CoachMarkBodyView` + // and `CoachMarkArrowView` protocols. + if !(bodyView is UIView) { + fatalError("Body view must conform to CoachMarkBodyView but also be a UIView.") + } + + if arrowView != nil && !(arrowView is UIView) { + fatalError("Arrow view must conform to CoachMarkArrowView but also be a UIView.") + } + + self.bodyView = bodyView + self.arrowView = arrowView + self.arrowOrientation = arrowOrientation + + if arrowOffset != nil { + self.arrowOffset = arrowOffset! + } + + super.init(frame: CGRectZero) + + self.bodyView.highlightArrowDelegate = self + self.layoutViewComposition() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("This class does not support NSCoding.") + } + + //MARK: - Protocol conformance | CoachMarkBodyHighlightArrowDelegate + + func highlightArrow(highlighted: Bool) { + self.arrowView?.highlighted = highlighted + } + + //MARK: - Internal Method + + //TODO: Better documentation + /// Change the arrow horizontal position to the given position. + /// `position` is relative to: + /// - `.Leading`: `offset` is relative to the leading edge of the overlay + /// - `.Center`: `offset` is relative to the center of the overlay + /// - `.Trailing`: `offset` is relative to the trailing edge of the overlay + /// + /// - Parameter position: arrow position + /// - Parameter offset: arrow offset + func changeArrowPositionTo(position: ArrowPosition, offset: CGFloat) { + if self.arrowView == nil { + return + } + + let arrowView = self.arrowView as! UIView + + if self.arrowXpositionConstraint != nil { + self.removeConstraint(self.arrowXpositionConstraint!) + } + + if position == .Leading { + self.arrowXpositionConstraint = + NSLayoutConstraint(item: arrowView, attribute: .CenterX, relatedBy: .Equal, + toItem: self.bodyView, attribute: .Leading, + multiplier: 1, constant: offset) + } else if position == .Center { + self.arrowXpositionConstraint = + NSLayoutConstraint(item: arrowView, attribute: .CenterX, relatedBy: .Equal, + toItem: self.bodyView, attribute: .CenterX, + multiplier: 1, constant: -offset) + } else if position == .Trailing { + self.arrowXpositionConstraint = + NSLayoutConstraint(item: arrowView, attribute: .CenterX, relatedBy: .Equal, + toItem: self.bodyView, attribute: .Trailing, + multiplier: 1, constant: -offset) + } + + self.addConstraint(self.arrowXpositionConstraint!) + } + + //MARK: - Private Method + + /// Layout the body view and the arrow view together. + private func layoutViewComposition() { + self.translatesAutoresizingMaskIntoConstraints = false + + self.addSubview(self.bodyView as! UIView) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[bodyView]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["bodyView": self.bodyView])) + + let bodyStickToTop = NSLayoutConstraint(item: self, attribute: .Top, relatedBy: .Equal, + toItem: self.bodyView, attribute: .Top, + multiplier: 1, constant: 0) + + let bodyStickToBottom = NSLayoutConstraint(item: self, attribute: .Bottom, relatedBy: .Equal, + toItem: self.bodyView, attribute: .Bottom, + multiplier: 1, constant: 0) + + if let arrowView = self.arrowView, arrowOrientation = self.arrowOrientation { + let arrowView = arrowView as! UIView + + self.addSubview(arrowView) + + self.arrowXpositionConstraint = NSLayoutConstraint(item: arrowView, attribute: .CenterX, relatedBy: .Equal, + toItem: self.bodyView, attribute: .CenterX, + multiplier: 1, constant: 0) + + self.addConstraint(self.arrowXpositionConstraint!) + + if arrowOrientation == .Top { + self.arrowStickToParent = NSLayoutConstraint(item: self, attribute: .Top, relatedBy: .Equal, + toItem: arrowView, attribute: .Top, + multiplier: 1, constant: 0) + + self.arrowStickToBodyConstraint = NSLayoutConstraint(item: arrowView, attribute: .Bottom, relatedBy: .Equal, + toItem: self.bodyView, attribute: .Top, + multiplier: 1, constant: self.arrowOffset) + + self.addConstraint(bodyStickToBottom) + } else if arrowOrientation == .Bottom { + self.arrowStickToParent = NSLayoutConstraint(item: self, attribute: .Bottom, relatedBy: .Equal, + toItem: arrowView, attribute: .Bottom, + multiplier: 1, constant: 0) + + self.arrowStickToBodyConstraint = NSLayoutConstraint(item: arrowView, attribute: .Top, relatedBy: .Equal, + toItem: self.bodyView, attribute: .Bottom, + multiplier: 1, constant: -self.arrowOffset) + + self.addConstraint(bodyStickToTop) + } + + self.addConstraint(self.arrowStickToParent!) + self.addConstraint(self.arrowStickToBodyConstraint!) + } else { + self.addConstraint(bodyStickToTop) + self.addConstraint(bodyStickToBottom) + } + } +} diff --git a/Source/CoachMarksController.swift b/Source/CoachMarksController.swift new file mode 100644 index 00000000..e8868c9e --- /dev/null +++ b/Source/CoachMarksController.swift @@ -0,0 +1,525 @@ +// CoachMarksController.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +/// Handles a set of coach marks, and display them successively. +public class CoachMarksController: UIViewController, OverlayViewDelegate { + //MARK: - Public properties + + /// `true` if coach marks are curently being displayed, `false` otherwise. + public var started: Bool { + return currentIndex != -1 + } + + /// An object implementing the data source protocol and supplying the coach marks to display. + public weak var datasource: CoachMarksControllerDataSource? + + /// An object implementing the delegate data source protocol, which methods will be called at various points. + public weak var delegate: CoachMarksControllerDelegate? + + /// Overlay fade animation duration + public var overlayFadeAnimationDuration = kOverlayFadeAnimationDuration + + /// Background color of the overlay. + public var overlayBackgroundColor: UIColor { + get { + return self.overlayView.overlayColor + } + + set { + self.overlayView.overlayColor = newValue + } + } + + /// Blur effect style for the overlay view. Keeping this property + /// `nil` will disable the effect. This property + /// is mutually exclusive with `overlayBackgroundColor`. + public var overlayBlurEffectStyle: UIBlurEffectStyle? { + get { + return self.overlayView.blurEffectStyle + } + + set { + self.overlayView.blurEffectStyle = newValue + } + } + + /// `true` to let the overlay catch tap event and forward them to the + /// CoachMarkController, `false` otherwise. + /// After receiving a tap event, the controller will show the next coach mark. + public var allowOverlayTap: Bool { + get { + return self.overlayView.allowOverlayTap + } + + set { + self.overlayView.allowOverlayTap = newValue + } + } + + //MARK: - Private properties + + /// The total number of coach marks, supplied by the `datasource`. + private var numberOfCoachMarks = 0; + + /// The index (in `coachMarks`) of the coach mark being currently displayed. + private var currentIndex = -1; + + /// Reference to the currently displayed coach mark, supplied by the `datasource`. + private var currentCoachMark: CoachMark? + + /// Reference to the currently displayed coach mark, supplied by the `datasource`. + private var currentCoachMarkView: CoachMarkView? + + /// The overlay view dim the background, handle the cutout path + /// showing the point of interest and also show up the coach marks themselve. + private lazy var overlayView: OverlayView = { + var overlayView = OverlayView() + overlayView.translatesAutoresizingMaskIntoConstraints = false + overlayView.delegate = self + + return overlayView + }() + + /// Sometimes, the chain of coach mark display can be paused + /// to let animations be performed. `true` to pause the execution, + /// `false` otherwise. + private var paused = false + + //MARK: - View lifecycle + + // Called after the view was loaded. + override public func viewDidLoad() { + super.viewDidLoad() + + self.view.translatesAutoresizingMaskIntoConstraints = false; + + self.addOverlayView() + } + + //MARK: - Public methods + + /// Start displaying the coach marks. + public func startOn(parentViewController: UIViewController) { + guard let datasource = self.datasource else { + print("Snap! You didn't setup any datasource, the coach mark manager won't do anything.") + return + } + + // If coach marks are currently being displayed, calling `start()` doesn't do anything. + if (self.started) { return } + + self.attachToViewController(parentViewController) + + // We make sure we are in a idle state and get the number of coach marks to display + // from the datasource. + self.currentIndex = -1; + self.numberOfCoachMarks = datasource.numberOfCoachMarksForCoachMarksController(self) + + // The view was previously hidden, to prevent it from catching the user input. + // Now, we want exactly the opposite. We want the overlay view to prevent events + // from reaching down. + self.view.userInteractionEnabled = true + + self.overlayView.prepareForFade() + UIView.animateWithDuration(self.overlayFadeAnimationDuration, animations: { () -> Void in + self.overlayView.alpha = 1.0 + }, completion: { (finished: Bool) -> Void in + self.showNextCoachMark() + }) + } + + /// Stop displaying the coach marks and perform some cleanup. + public func stop() { + UIView.animateWithDuration(self.overlayFadeAnimationDuration, animations: { () -> Void in + self.overlayView.alpha = 0.0 + }, completion: {(finished: Bool) -> Void in + self.reset() + self.detachFromViewController() + + // Calling the delegate, maybe the user wants to do something? + self.delegate?.didFinishShowingFromCoachMarksController(self) + + }) + } + + //MARK: - Protocol Conformance | OverlayViewDelegate + + internal func didReceivedSingleTap() { + if self.paused { return } + + self.showNextCoachMark(); + } + + /// Will be called when the user perform an action requiring the display of the next coach mark. + /// + /// - Parameter sender: the object sending the message + public func performShowNextCoachMark(sender:AnyObject?) { + self.showNextCoachMark(); + } + + //MARK: - Private methods + + /// Show the next coach mark and hide the current one. + private func showNextCoachMark() { + self.currentIndex++ + + // if `currentIndex` is above 0, that means a previous coach mark + // is displayed. We call the delegate to notify that the current coach + // mark will disappear, and only then, we hide the coach mark. + if self.currentIndex > 0 { + self.delegate?.coachMarksController(self, coachMarkWillDisappear: self.currentCoachMark!, forIndex: self.currentIndex - 1) + + self.overlayView.hideCutoutPathViewWithAnimationDuration(self.currentCoachMark!.animationDuration) + + UIView.animateWithDuration(self.currentCoachMark!.animationDuration, animations: { () -> Void in + self.currentCoachMarkView?.alpha = 0.0 + }, completion: {(finished: Bool) -> Void in + self.currentCoachMarkView?.removeFromSuperview() + self.currentCoachMarkView?.nextControl?.removeTarget(self, action: "performShowNextCoachMark:", forControlEvents: .TouchUpInside) + + if self.currentIndex < self.numberOfCoachMarks { + self.retrieveCoachMarkFromDataSource() + } else { + self.stop() + } + }) + } else { + self.retrieveCoachMarkFromDataSource() + } + } + + /// Will attach the controller as a child of the given view controller. This will + /// allow the coach mark controller to part of UIKit chain. + /// + /// - Parameter parentViewController: the controller of which become a child + public func attachToViewController(parentViewController: UIViewController) { + parentViewController.addChildViewController(self) + parentViewController.view.addSubview(self.view) + + parentViewController.view.addConstraints( + NSLayoutConstraint.constraintsWithVisualFormat("V:|[overlayView]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["overlayView": self.view])) + + parentViewController.view.addConstraints( + NSLayoutConstraint.constraintsWithVisualFormat("H:|[overlayView]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["overlayView": self.view])) + + self.beginAppearanceTransition(true, animated: false) + self.endAppearanceTransition() + + self.didMoveToParentViewController(parentViewController) + } + + /// Detach the controller from its parent view controller. + public func detachFromViewController() { + self.beginAppearanceTransition(false, animated: false) + self.endAppearanceTransition() + + self.willMoveToParentViewController(nil) + self.view.removeFromSuperview() + self.removeFromParentViewController() + } + + /// Returns a new coach mark with a cutout path set to be + /// around the provided UIView. The cutout path will be slightly + /// larger than the view and have rounded corners, however you can + /// bypass the default creator by providing a block. + /// + /// The point of interest (defining where the arrow will sit, horizontally) + /// will be set at the center of the cutout path. + /// + /// - Parameters view: the view around which create the cutoutPath + /// - Parameters bezierPathBlock: a block customizing the cutoutPath + public func coachMarkForView(view: UIView? = nil, bezierPathBlock: ((frame: CGRect) -> UIBezierPath)? = nil) -> CoachMark { + return self.coachMarkForView(view, pointOfInterest: nil, bezierPathBlock: bezierPathBlock) + } + + /// Returns a new coach mark with a cutout path set to be + /// around the provided UIView. The cutout path will be slightly + /// larger than the view and have rounded corners, however you can + /// bypass the default creator by providing a block. + /// + /// The point of interest (defining where the arrow will sit, horizontally) + /// will be the one provided. + /// + /// - Parameters view: the view around which create the cutoutPath + /// - Parameters pointOfInterest: the point of interest toward which the arrow should point + /// - Parameters bezierPathBlock: a block customizing the cutoutPath + public func coachMarkForView(view: UIView? = nil, pointOfInterest: CGPoint?, bezierPathBlock: ((frame: CGRect) -> UIBezierPath)? = nil) -> CoachMark { + var coachMark = CoachMark() + + guard let view = view else { + return coachMark + } + + self.updateCoachMark(&coachMark, forView: view, pointOfInterest: pointOfInterest, bezierPathBlock: bezierPathBlock) + + return coachMark + } + + /// Updates the currently stored coach mark with a cutout path set to be + /// around the provided UIView. The cutout path will be slightly + /// larger than the view and have rounded corners, however you can + /// bypass the default creator by providing a block. + /// + /// The point of interest (defining where the arrow will sit, horizontally) + /// will be the one provided. + /// + /// This method is expected to be used in the delegate, after pausing the display. + /// Otherwise, there might not be such a thing as a "current coach mark". + /// + /// - Parameters view: the view around which create the cutoutPath + /// - Parameters pointOfInterest: the point of interest toward which the arrow should point + /// - Parameters bezierPathBlock: a block customizing the cutoutPath + public func updateCurrentCoachMarkForView(view: UIView? = nil, pointOfInterest: CGPoint? = nil, bezierPathBlock: ((frame: CGRect) -> UIBezierPath)? = nil) -> Void { + if !self.paused || self.currentCoachMark == nil { + print("Something is wrong, did you called updateCurrentCoachMarkForView without pausing the controller first?") + return + } + + self.updateCoachMark(&self.currentCoachMark!, forView: view, pointOfInterest: pointOfInterest, bezierPathBlock: bezierPathBlock) + } + + /// Updates the given coach mark with a cutout path set to be + /// around the provided UIView. The cutout path will be slightly + /// larger than the view and have rounded corners, however you can + /// bypass the default creator by providing a block. + /// + /// The point of interest (defining where the arrow will sit, horizontally) + /// will be the one provided. + /// + /// - Parameters coachMark: the CoachMark to update + /// - Parameters view: the view around which create the cutoutPath + /// - Parameters pointOfInterest: the point of interest toward which the arrow should point + /// - Parameters bezierPathBlock: a block customizing the cutoutPath + public func updateCoachMark(inout coachMark: CoachMark, forView view: UIView? = nil, pointOfInterest: CGPoint?, bezierPathBlock: ((frame: CGRect) -> UIBezierPath)? = nil) -> Void { + + guard let view = view else { + return + } + + let convertedFrame = self.view.convertRect(view.frame, fromView:view.superview); + + var bezierPath: UIBezierPath; + + if let bezierPathBlock = bezierPathBlock { + bezierPath = bezierPathBlock(frame: convertedFrame) + } else { + bezierPath = UIBezierPath(roundedRect: CGRectInset(convertedFrame, -4, -4), byRoundingCorners: .AllCorners, cornerRadii: CGSize(width: 4, height: 4)) + } + + coachMark.cutoutPath = bezierPath + + if let pointOfInterest = pointOfInterest { + let convertedPoint = self.view.convertPoint(pointOfInterest, fromView:view.superview); + coachMark.pointOfInterest = convertedPoint + } + } + + /// Provides default coach views. + /// + /// - Parameter withArrow: `true` to return an instance of `CoachMarkArrowDefaultView` as well, `false` otherwise. + /// - Parameter arrowOrientation: orientation of the arrow (either .Top or .Bottom) + /// + /// - Returns: new instances of the default coach views. + public func defaultCoachViewsWithArrow(withArrow: Bool = true, arrowOrientation: CoachMarkArrowOrientation? = .Top) -> (bodyView: CoachMarkBodyDefaultView, arrowView: CoachMarkArrowDefaultView?) { + + let coachMarkBodyView = CoachMarkBodyDefaultView() + + var coachMarkArrowView: CoachMarkArrowDefaultView? = nil + + if withArrow { + var arrowOrientation = arrowOrientation + + if arrowOrientation == nil { + arrowOrientation = .Top + } + + coachMarkArrowView = CoachMarkArrowDefaultView(orientation: arrowOrientation!) + } + + return (bodyView: coachMarkBodyView, arrowView: coachMarkArrowView) + } + + /// Pause the display. + /// This method is expected to be used by the delegate to + /// top the display, perform animation and resume display with `play()` + public func pause() { + self.paused = true + } + + /// Play the display. + /// If the display wasn't paused earlier, this method won't do anything. + public func play() { + if self.started && self.paused { + self.paused = false + self.createAndShowCoachMark() + } + } + + //MARK: - Private methods + /// Add the overlay view which will blur/dim the background. + private func addOverlayView() { + self.view.addSubview(self.overlayView) + + self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[overlayView]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["overlayView": self.overlayView])) + + self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[overlayView]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["overlayView": self.overlayView])) + + self.overlayView.alpha = 0.0 + } + + /// Return the controller into an idle state. + private func reset() { + self.numberOfCoachMarks = 0; + self.currentIndex = -1; + + self.currentCoachMark = nil; + self.currentCoachMarkView = nil; + } + + /// Ask the datasource, create the coach mark and display it. Also + /// notifies the delegate. + private func retrieveCoachMarkFromDataSource() { + // Retrieves the current coach mark structure from the datasource. + // It can't be nil, that's why we'll force unwrap it everywhere. + self.currentCoachMark = self.datasource!.coachMarksController(self, coachMarksForIndex: self.currentIndex) + + // The coach mark will soon show, we notify the delegate, so it + // can peform some things and, if required, update the coach mark structure. + self.delegate?.coachMarksController(self, coachMarkWillShow: &self.currentCoachMark!, forIndex: self.currentIndex) + + if !self.paused { + createAndShowCoachMark() + } + } + + /// Create display the coach mark view. + private func createAndShowCoachMark() { + + // Once the coach mark structure is final, we'll compute the arrow + // orientation, so the data source will know what king of views supply. + self.currentCoachMark!.computeOrientationInFrame(self.view.frame) + + // Compute the point of interest, based on the cutOut path. + self.currentCoachMark!.computePointOfInterestInFrame() + + let coachMark = self.currentCoachMark! + + // Asksthe data source for the appropriate tuple of views. + let coachMarkComponentViews = self.datasource!.coachMarksController(self, coachMarkViewsForIndex: self.currentIndex, coachMark: coachMark) + + // Creates the CoachMarkView, from the supplied component views. + // CoachMarkView() is not a failable initializer. We'll force unwrap + // currentCoachMarkView everywhere. + self.currentCoachMarkView = CoachMarkView(bodyView: coachMarkComponentViews.bodyView, arrowView: coachMarkComponentViews.arrowView, arrowOrientation: coachMark.arrowOrientation, arrowOffset: coachMark.gapBetweenBodyAndArrow) + + let coachMarkView = self.currentCoachMarkView! + + // Hook up the next coach control. + coachMarkView.nextControl?.addTarget(self, action: "performShowNextCoachMark:", forControlEvents: .TouchUpInside) + + // The view shall be invisible, 'cause we'll animate its entry. + coachMarkView.alpha = 0.0 + + self.prepareCurrentCoachMarkForDisplay() + + // Animate the view entry + self.overlayView.showCutoutPathViewWithAnimationDuration(coachMark.animationDuration) + + UIView.animateWithDuration(coachMark.animationDuration) { () -> Void in + self.currentCoachMarkView!.alpha = 1.0 + } + } + + /// Add the current coach mark to the view, making sure it is + /// properly positioned. + private func prepareCurrentCoachMarkForDisplay() { + guard let coachMark = self.currentCoachMark, coachMarkView = self.currentCoachMarkView else { + return + } + + // Add the view and compute its associated constraints. + self.view.addSubview(coachMarkView) + + self.view.addConstraints( + NSLayoutConstraint.constraintsWithVisualFormat("H:[currentCoachMarkView(<=\(coachMark.maxWidth))]", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["currentCoachMarkView": self.currentCoachMarkView!]) + ) + + // No cutoutPath, no arrow. + if let cutoutPath = coachMark.cutoutPath { + let offset = coachMark.gapBetweenCoachMarkAndCutoutPath + + // Depending where the cutoutPath sits, the coach mark will either + // stand above or below it. + if coachMark.arrowOrientation! == .Bottom { + let coachMarkViewConstraint = NSLayoutConstraint(item: coachMarkView, attribute: .Bottom, relatedBy: .Equal, toItem: self.view, attribute: .Bottom, multiplier: 1, constant: -(self.view.frame.size.height - cutoutPath.bounds.origin.y + offset)) + self.view.addConstraint(coachMarkViewConstraint) + } else { + let coachMarkViewConstraint = NSLayoutConstraint(item: coachMarkView, attribute: .Top, relatedBy: .Equal, toItem: self.view, attribute: .Top, multiplier: 1, constant: (cutoutPath.bounds.origin.y + cutoutPath.bounds.size.height) + offset) + self.view.addConstraint(coachMarkViewConstraint) + } + + let horizontalMargin = coachMark.horizontalMargin + let maxWidth = coachMark.maxWidth + + let pointOfInterest = coachMark.pointOfInterest! + let segmentNumber = 3 * pointOfInterest.x / self.view.bounds.size.width + + if segmentNumber < 1 { + self.view.addConstraints( + NSLayoutConstraint.constraintsWithVisualFormat("H:|-(==\(horizontalMargin))-[currentCoachMarkView(<=\(maxWidth))]-(>=\(horizontalMargin))-|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["currentCoachMarkView": coachMarkView]) + ) + + coachMarkView.changeArrowPositionTo(CoachMarkView.ArrowPosition.Leading, offset: pointOfInterest.x - coachMark.horizontalMargin) + + } else if segmentNumber < 2 { + self.view.addConstraint(NSLayoutConstraint(item: coachMarkView, attribute: .CenterX, relatedBy: .Equal, toItem: self.view, attribute: .CenterX, multiplier: 1, constant: 0)) + + self.view.addConstraints( + NSLayoutConstraint.constraintsWithVisualFormat("H:|-(>=\(horizontalMargin))-[currentCoachMarkView(<=\(maxWidth)@1000)]-(>=\(horizontalMargin))-|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["currentCoachMarkView": coachMarkView]) + ) + + coachMarkView.changeArrowPositionTo(CoachMarkView.ArrowPosition.Center, offset: self.view.center.x - pointOfInterest.x) + + } else if segmentNumber < 3 { + self.view.addConstraints( + NSLayoutConstraint.constraintsWithVisualFormat("H:|-(>=\(horizontalMargin))-[currentCoachMarkView(<=\(maxWidth))]-(==\(horizontalMargin))-|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["currentCoachMarkView": coachMarkView]) + ) + + coachMarkView.changeArrowPositionTo(CoachMarkView.ArrowPosition.Trailing, offset: self.view.bounds.size.width - pointOfInterest.x - coachMark.horizontalMargin) + } + + self.overlayView.updateCutoutPath(cutoutPath) + } else { + self.overlayView.updateCutoutPath(nil) + } + } +} diff --git a/Source/CoachMarksControllerDataSource.swift b/Source/CoachMarksControllerDataSource.swift new file mode 100644 index 00000000..b02a4686 --- /dev/null +++ b/Source/CoachMarksControllerDataSource.swift @@ -0,0 +1,57 @@ +// CoachMarksControllerDataSource.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Describe how a coachmark datasource should behave. +/// It works a bit like `UITableViewDataSource`. +public protocol CoachMarksControllerDataSource: class { + /// Asks for the number of coach marks to display. + /// + /// - Parameter coachMarksController: the coach mark controller requesting the information + /// + /// - Returns: the number of coach marks to display + func numberOfCoachMarksForCoachMarksController(coachMarksController: CoachMarksController) -> Int + + /// Asks for the metadata of the coach mark that will be displayed in the given nth place. + /// All `CoachMark` metadata are optional or filled with sensible defaults. You are not + /// forced to provide the `cutoutPath`. If you don't the coach mark will be dispayed at the bottom + /// of the screen, without an arrow. + /// + /// - Parameter coachMarksController: the coach mark controller requesting the information + /// - Parameter coachMarkViewsForIndex: the index referring to the nth place + /// + /// - Returns: the coach mark metadata + func coachMarksController(coachMarksController: CoachMarksController, coachMarksForIndex index: Int) -> CoachMark + + /// Asks for the views defining the coach mark that will be displayed in the given nth place. + /// The arrow view is optional. However, if you provide one, you are responsible for + /// supplying the proper arrow orientation. The expected orientation + /// is available through `coachMark.arrowOrientation` and was computed beforehand. + /// + /// - Parameter coachMarksController: the coach mark controller requesting the information + /// - Parameter coachMarkViewsForIndex: the index referring to the nth place + /// - Parameter coachMark: the coach mark meta data + /// + /// - Returns: a tuple packaging the body component and the arrow component + func coachMarksController(coachMarksController: CoachMarksController, coachMarkViewsForIndex index: Int, coachMark: CoachMark) -> (bodyView: CoachMarkBodyView, arrowView: CoachMarkArrowView?) +} \ No newline at end of file diff --git a/Source/CoachMarksControllerDelegate.swift b/Source/CoachMarksControllerDelegate.swift new file mode 100644 index 00000000..bd0070da --- /dev/null +++ b/Source/CoachMarksControllerDelegate.swift @@ -0,0 +1,40 @@ +// CoachMarksControllerDelegate.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Give a chance to react when coach marks are displayed +public protocol CoachMarksControllerDelegate: class { + func coachMarksController(coachMarksController: CoachMarksController, inout coachMarkWillShow coachMark: CoachMark, forIndex index: Int) + + func coachMarksController(coachMarksController: CoachMarksController, coachMarkWillDisappear coachMark: CoachMark, forIndex index: Int) + + func didFinishShowingFromCoachMarksController(coachMarksController: CoachMarksController) +} + +public extension CoachMarksControllerDelegate { + func coachMarksController(coachMarksController: CoachMarksController, inout coachMarkWillShow coachMark: CoachMark, forIndex index: Int) {} + + func coachMarksController(coachMarksController: CoachMarksController, coachMarkWillDisappear coachMark: CoachMark, forIndex index: Int) {} + + func didFinishShowingFromCoachMarksController(coachMarksController: CoachMarksController) {} +} \ No newline at end of file diff --git a/Source/Images.xcassets/Contents.json b/Source/Images.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Source/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Source/Images.xcassets/arrow-bottom-highlighted.imageset/Contents.json b/Source/Images.xcassets/arrow-bottom-highlighted.imageset/Contents.json new file mode 100644 index 00000000..3e34333a --- /dev/null +++ b/Source/Images.xcassets/arrow-bottom-highlighted.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bubble-arrow-bottom-highlighted.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bubble-arrow-bottom-highlighted@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bubble-arrow-bottom-highlighted@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Source/Images.xcassets/arrow-bottom-highlighted.imageset/bubble-arrow-bottom-highlighted.png b/Source/Images.xcassets/arrow-bottom-highlighted.imageset/bubble-arrow-bottom-highlighted.png new file mode 100644 index 00000000..0506d95a Binary files /dev/null and b/Source/Images.xcassets/arrow-bottom-highlighted.imageset/bubble-arrow-bottom-highlighted.png differ diff --git a/Source/Images.xcassets/arrow-bottom-highlighted.imageset/bubble-arrow-bottom-highlighted@2x.png b/Source/Images.xcassets/arrow-bottom-highlighted.imageset/bubble-arrow-bottom-highlighted@2x.png new file mode 100644 index 00000000..444b7887 Binary files /dev/null and b/Source/Images.xcassets/arrow-bottom-highlighted.imageset/bubble-arrow-bottom-highlighted@2x.png differ diff --git a/Source/Images.xcassets/arrow-bottom-highlighted.imageset/bubble-arrow-bottom-highlighted@3x.png b/Source/Images.xcassets/arrow-bottom-highlighted.imageset/bubble-arrow-bottom-highlighted@3x.png new file mode 100644 index 00000000..9c899904 Binary files /dev/null and b/Source/Images.xcassets/arrow-bottom-highlighted.imageset/bubble-arrow-bottom-highlighted@3x.png differ diff --git a/Source/Images.xcassets/arrow-bottom.imageset/Contents.json b/Source/Images.xcassets/arrow-bottom.imageset/Contents.json new file mode 100644 index 00000000..c68f8cfc --- /dev/null +++ b/Source/Images.xcassets/arrow-bottom.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bubble-arrow-bottom.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bubble-arrow-bottom@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bubble-arrow-bottom@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Source/Images.xcassets/arrow-bottom.imageset/bubble-arrow-bottom.png b/Source/Images.xcassets/arrow-bottom.imageset/bubble-arrow-bottom.png new file mode 100644 index 00000000..52f2612f Binary files /dev/null and b/Source/Images.xcassets/arrow-bottom.imageset/bubble-arrow-bottom.png differ diff --git a/Source/Images.xcassets/arrow-bottom.imageset/bubble-arrow-bottom@2x.png b/Source/Images.xcassets/arrow-bottom.imageset/bubble-arrow-bottom@2x.png new file mode 100644 index 00000000..b94aef52 Binary files /dev/null and b/Source/Images.xcassets/arrow-bottom.imageset/bubble-arrow-bottom@2x.png differ diff --git a/Source/Images.xcassets/arrow-bottom.imageset/bubble-arrow-bottom@3x.png b/Source/Images.xcassets/arrow-bottom.imageset/bubble-arrow-bottom@3x.png new file mode 100644 index 00000000..a8f160e0 Binary files /dev/null and b/Source/Images.xcassets/arrow-bottom.imageset/bubble-arrow-bottom@3x.png differ diff --git a/Source/Images.xcassets/arrow-top-highlighted.imageset/Contents.json b/Source/Images.xcassets/arrow-top-highlighted.imageset/Contents.json new file mode 100644 index 00000000..9f6985da --- /dev/null +++ b/Source/Images.xcassets/arrow-top-highlighted.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bubble-arrow-top-highlighted.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bubble-arrow-top-highlighted@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bubble-arrow-top-highlighted@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Source/Images.xcassets/arrow-top-highlighted.imageset/bubble-arrow-top-highlighted.png b/Source/Images.xcassets/arrow-top-highlighted.imageset/bubble-arrow-top-highlighted.png new file mode 100644 index 00000000..2a6af962 Binary files /dev/null and b/Source/Images.xcassets/arrow-top-highlighted.imageset/bubble-arrow-top-highlighted.png differ diff --git a/Source/Images.xcassets/arrow-top-highlighted.imageset/bubble-arrow-top-highlighted@2x.png b/Source/Images.xcassets/arrow-top-highlighted.imageset/bubble-arrow-top-highlighted@2x.png new file mode 100644 index 00000000..f87edb3f Binary files /dev/null and b/Source/Images.xcassets/arrow-top-highlighted.imageset/bubble-arrow-top-highlighted@2x.png differ diff --git a/Source/Images.xcassets/arrow-top-highlighted.imageset/bubble-arrow-top-highlighted@3x.png b/Source/Images.xcassets/arrow-top-highlighted.imageset/bubble-arrow-top-highlighted@3x.png new file mode 100644 index 00000000..36214046 Binary files /dev/null and b/Source/Images.xcassets/arrow-top-highlighted.imageset/bubble-arrow-top-highlighted@3x.png differ diff --git a/Source/Images.xcassets/arrow-top.imageset/Contents.json b/Source/Images.xcassets/arrow-top.imageset/Contents.json new file mode 100644 index 00000000..e33d2b46 --- /dev/null +++ b/Source/Images.xcassets/arrow-top.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bubble-arrow-top.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bubble-arrow-top@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bubble-arrow-top@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Source/Images.xcassets/arrow-top.imageset/bubble-arrow-top.png b/Source/Images.xcassets/arrow-top.imageset/bubble-arrow-top.png new file mode 100644 index 00000000..127d7c2c Binary files /dev/null and b/Source/Images.xcassets/arrow-top.imageset/bubble-arrow-top.png differ diff --git a/Source/Images.xcassets/arrow-top.imageset/bubble-arrow-top@2x.png b/Source/Images.xcassets/arrow-top.imageset/bubble-arrow-top@2x.png new file mode 100644 index 00000000..73b37bf2 Binary files /dev/null and b/Source/Images.xcassets/arrow-top.imageset/bubble-arrow-top@2x.png differ diff --git a/Source/Images.xcassets/arrow-top.imageset/bubble-arrow-top@3x.png b/Source/Images.xcassets/arrow-top.imageset/bubble-arrow-top@3x.png new file mode 100644 index 00000000..1b707ed8 Binary files /dev/null and b/Source/Images.xcassets/arrow-top.imageset/bubble-arrow-top@3x.png differ diff --git a/Source/Images.xcassets/background-highlighted.imageset/Contents.json b/Source/Images.xcassets/background-highlighted.imageset/Contents.json new file mode 100644 index 00000000..624b807b --- /dev/null +++ b/Source/Images.xcassets/background-highlighted.imageset/Contents.json @@ -0,0 +1,65 @@ +{ + "images" : [ + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 9, + "top" : 8, + "right" : 8, + "left" : 8 + } + }, + "idiom" : "universal", + "filename" : "pill-medium-letter-highlighted.png", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 2, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 18, + "top" : 17, + "right" : 16, + "left" : 16 + } + }, + "idiom" : "universal", + "filename" : "pill-medium-letter-highlighted@2x.png", + "scale" : "2x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 27, + "top" : 26, + "right" : 25, + "left" : 25 + } + }, + "idiom" : "universal", + "filename" : "pill-medium-letter-highlighted@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Source/Images.xcassets/background-highlighted.imageset/pill-medium-letter-highlighted.png b/Source/Images.xcassets/background-highlighted.imageset/pill-medium-letter-highlighted.png new file mode 100644 index 00000000..1ae65fd5 Binary files /dev/null and b/Source/Images.xcassets/background-highlighted.imageset/pill-medium-letter-highlighted.png differ diff --git a/Source/Images.xcassets/background-highlighted.imageset/pill-medium-letter-highlighted@2x.png b/Source/Images.xcassets/background-highlighted.imageset/pill-medium-letter-highlighted@2x.png new file mode 100644 index 00000000..2738d7da Binary files /dev/null and b/Source/Images.xcassets/background-highlighted.imageset/pill-medium-letter-highlighted@2x.png differ diff --git a/Source/Images.xcassets/background-highlighted.imageset/pill-medium-letter-highlighted@3x.png b/Source/Images.xcassets/background-highlighted.imageset/pill-medium-letter-highlighted@3x.png new file mode 100644 index 00000000..f52e0a22 Binary files /dev/null and b/Source/Images.xcassets/background-highlighted.imageset/pill-medium-letter-highlighted@3x.png differ diff --git a/Source/Images.xcassets/background.imageset/Contents.json b/Source/Images.xcassets/background.imageset/Contents.json new file mode 100644 index 00000000..696bff5c --- /dev/null +++ b/Source/Images.xcassets/background.imageset/Contents.json @@ -0,0 +1,65 @@ +{ + "images" : [ + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 9, + "top" : 8, + "right" : 8, + "left" : 8 + } + }, + "idiom" : "universal", + "filename" : "pill-medium-letter.png", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 2, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 18, + "top" : 17, + "right" : 16, + "left" : 16 + } + }, + "idiom" : "universal", + "filename" : "pill-medium-letter@2x.png", + "scale" : "2x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "tile", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 27, + "top" : 26, + "right" : 25, + "left" : 25 + } + }, + "idiom" : "universal", + "filename" : "pill-medium-letter@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Source/Images.xcassets/background.imageset/pill-medium-letter.png b/Source/Images.xcassets/background.imageset/pill-medium-letter.png new file mode 100644 index 00000000..8e2c7f04 Binary files /dev/null and b/Source/Images.xcassets/background.imageset/pill-medium-letter.png differ diff --git a/Source/Images.xcassets/background.imageset/pill-medium-letter@2x.png b/Source/Images.xcassets/background.imageset/pill-medium-letter@2x.png new file mode 100644 index 00000000..162742e8 Binary files /dev/null and b/Source/Images.xcassets/background.imageset/pill-medium-letter@2x.png differ diff --git a/Source/Images.xcassets/background.imageset/pill-medium-letter@3x.png b/Source/Images.xcassets/background.imageset/pill-medium-letter@3x.png new file mode 100644 index 00000000..f0d03f9e Binary files /dev/null and b/Source/Images.xcassets/background.imageset/pill-medium-letter@3x.png differ diff --git a/Source/Instructions.h b/Source/Instructions.h index 76909686..3fe0d529 100644 --- a/Source/Instructions.h +++ b/Source/Instructions.h @@ -1,10 +1,24 @@ +// Instructions.h // -// Instructions.h -// Instructions +// Copyright (c) 2015 Frédéric Maquin // -// Created by Frédéric Maquin on 26/09/15. -// Copyright © 2015 Ephread. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: // +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. #import diff --git a/Source/Instructions.swift b/Source/Instructions.swift new file mode 100644 index 00000000..4de3d65a --- /dev/null +++ b/Source/Instructions.swift @@ -0,0 +1,43 @@ +// Instructions.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Execute some code after a given delay +/// +/// From [Matt Neuburg](http://stackoverflow.com/users/341994/matt), on +/// [Stack Overflow](http://stackoverflow.com/questions/24034544/dispatch-after-gcd-in-swift/24318861#24318861) +/// +/// - Parameter delay: the time to wait before executing +/// - Parameter closure: the closure to execute +public func delay(delay:Double, closure:()->()) { + dispatch_after( + dispatch_time( + DISPATCH_TIME_NOW, + Int64(delay * Double(NSEC_PER_SEC)) + ), + dispatch_get_main_queue(), closure) +} + +let kOverlayFadeAnimationDuration: NSTimeInterval = 0.3; +let kCoachMarkFadeAnimationDuration: NSTimeInterval = 0.3; +let kOverlayColor = UIColor(red: 226.0/255.0, green: 226.0/255.0, blue: 226.0/255.0, alpha: 0.65); diff --git a/Source/OverlayView.swift b/Source/OverlayView.swift new file mode 100644 index 00000000..1d49d084 --- /dev/null +++ b/Source/OverlayView.swift @@ -0,0 +1,239 @@ +// OverlayView.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +// Overlay a blocking view on top of the screen and handle the cutout path +// around the point of interest. +internal class OverlayView: UIView { + //MARK: - Internal properties + + /// The background color of the overlay + var overlayColor: UIColor = kOverlayColor + + /// The blur effect style to apply to the overlay. + /// Setting this property to anything but `nil` will + /// enable the effect. `overlayColor` will be ignored if this + /// property is set. + var blurEffectStyle: UIBlurEffectStyle? { + didSet { + if self.blurEffectStyle != oldValue { + self.destroyBlurView() + self.createBlurView() + } + } + } + + /// `true` to let the overlay catch tap event and forward them to the + /// CoachMarkController, `false` otherwise. + /// After receiving a tap event, the controller will show the next coach mark. + var allowOverlayTap: Bool { + get { + return self.singleTapGestureRecognizer.view != nil + } + + set { + if newValue == true { + self.addGestureRecognizer(self.singleTapGestureRecognizer) + } else { + self.removeGestureRecognizer(self.singleTapGestureRecognizer) + } + } + } + + /// Delegate to which tell that the overlay view received a tap event. + weak var delegate: OverlayViewDelegate? + + //MARK: - Private properties + + /// The cutout mask + private var cutoutMaskLayer = CAShapeLayer() + + /// The full mask (together with `cutoutMaskLayer` they will form the cutout shape) + private var fullMaskLayer = CAShapeLayer() + + /// The overlay layer, which will handle the background color + private var overlayLayer = CALayer() + + /// The view holding the blur effect + private var blurEffectView: UIVisualEffectView? + + /// TapGestureRecognizer that will catch tap event performed on the overlay + private lazy var singleTapGestureRecognizer: UITapGestureRecognizer = { + let gestureRecognizer = UITapGestureRecognizer(target: self, action: "handleSingleTap:") + + return gestureRecognizer + }() + + //MARK: - Initialization + init() { + super.init(frame: CGRectZero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("This class does not support NSCoding") + } + + //MARK: - Internal methods + + /// Prepare for the fade, by removing the cutout shape. + func prepareForFade() { + self.updateCutoutPath(nil) + } + + /// Show a cutout path with fade in animation + /// + /// - Parameter duration: duration of the animation + func showCutoutPathViewWithAnimationDuration(duration: NSTimeInterval) { + CATransaction.begin() + + self.fullMaskLayer.opacity = 0.0 + + let animation = CABasicAnimation(keyPath: "opacity") + animation.fromValue = 1.0 + animation.toValue = 0.0 + animation.duration = duration + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + animation.removedOnCompletion = true + + self.fullMaskLayer.addAnimation(animation, forKey: "opacityAnimationFadeIn") + + CATransaction.commit() + } + + /// Hide a cutout path with fade in animation + /// + /// - Parameter duration: duration of the animation + func hideCutoutPathViewWithAnimationDuration(duration: NSTimeInterval) { + CATransaction.begin() + + self.fullMaskLayer.opacity = 1.0 + + let animation = CABasicAnimation(keyPath: "opacity") + animation.fromValue = 0.0 + animation.toValue = 1.0 + animation.duration = duration + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + animation.removedOnCompletion = true + + self.fullMaskLayer.addAnimation(animation, forKey: "opacityAnimationFadeOut") + + CATransaction.commit() + } + + /// Update the cutout path. Please note that the update won't perform any + /// interpolation. The previous cutout path better be hidden or else, + /// some jaggy effects are to be expected. + /// + /// - Parameter cutoutPath: the cutout path + func updateCutoutPath(cutoutPath: UIBezierPath?) { + + self.cutoutMaskLayer.removeFromSuperlayer() + self.fullMaskLayer.removeFromSuperlayer() + self.overlayLayer.removeFromSuperlayer() + + if cutoutPath == nil { + if self.blurEffectView == nil { + self.backgroundColor = self.overlayColor + } + + return + } + + self.backgroundColor = UIColor.clearColor() + + self.cutoutMaskLayer = CAShapeLayer() + self.cutoutMaskLayer.name = "cutoutMaskLayer" + self.cutoutMaskLayer.fillRule = kCAFillRuleEvenOdd + self.cutoutMaskLayer.frame = self.frame + + self.fullMaskLayer = CAShapeLayer() + self.fullMaskLayer.name = "fullMaskLayer" + self.fullMaskLayer.fillRule = kCAFillRuleEvenOdd + self.fullMaskLayer.frame = self.frame + self.fullMaskLayer.opacity = 1.0 + + let cutoutMaskLayerPath = UIBezierPath() + cutoutMaskLayerPath.appendPath(UIBezierPath(rect: self.bounds)) + cutoutMaskLayerPath.appendPath(cutoutPath!) + + let fullMaskLayerPath = UIBezierPath() + fullMaskLayerPath.appendPath(UIBezierPath(rect: self.bounds)) + + self.cutoutMaskLayer.path = cutoutMaskLayerPath.CGPath + self.fullMaskLayer.path = fullMaskLayerPath.CGPath + + let maskLayer = CALayer(); + maskLayer.frame = self.layer.bounds; + maskLayer.addSublayer(self.cutoutMaskLayer) + maskLayer.addSublayer(self.fullMaskLayer) + + self.overlayLayer = CALayer(); + self.overlayLayer.frame = self.layer.bounds + + if self.blurEffectView == nil { + self.overlayLayer.backgroundColor = self.overlayColor.CGColor + } + + self.overlayLayer.mask = maskLayer + + if let blurEffectView = self.blurEffectView { + blurEffectView.layer.mask = maskLayer + } else { + self.layer.addSublayer(self.overlayLayer) + } + } + + //MARK: - Private Methods + + /// Creates the visual effect view holding + /// the blur effect and adds it to the overlay. + private func createBlurView() { + if self.blurEffectStyle == nil { return } + + let blurEffect = UIBlurEffect(style: self.blurEffectStyle!) + + self.blurEffectView = UIVisualEffectView(effect:blurEffect) + self.blurEffectView!.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(self.blurEffectView!) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[visualEffectView]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["visualEffectView": self.blurEffectView!])) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[visualEffectView]|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["visualEffectView": self.blurEffectView!])) + } + + /// Removes the view holding the blur effect. + private func destroyBlurView() { + self.blurEffectView?.removeFromSuperview() + self.blurEffectView = nil + } + + /// This method will be called each time the overlay receive + /// a tap event. + /// + /// - Parameter sender: the object which sent the event + @objc private func handleSingleTap(sender: AnyObject?) { + self.delegate?.didReceivedSingleTap() + } +} diff --git a/Source/OverlayViewController.swift b/Source/OverlayViewController.swift new file mode 100644 index 00000000..746ab3e0 --- /dev/null +++ b/Source/OverlayViewController.swift @@ -0,0 +1,105 @@ +// +// OverlayViewController.swift +// Instructions +// +// Created by Frédéric Maquin on 27/09/15. +// Copyright © 2015 Ephread. All rights reserved. +// + +import Foundation + +class OverlayViewController: NSObject { + let overlayView: OverlayView + + private(set) var coachViewConstraint: [NSLayoutConstraint] = [] + private(set) var coachMarks: [CoachMark] = [] + private(set) var coachMarkViews: [CoachMarkView] = [] + + //MARK: - Private properties + private var overlayLayers: [CALayer] = [] + private var fullMaskLayers: [CAShapeLayer] = [] + + private let backgroundView: UIView = { + let backgroundView = UIView() + + backgroundView.translatesAutoresizingMaskIntoConstraints = false; + backgroundView.backgroundColor = UIColor.clearColor() + backgroundView.userInteractionEnabled = false; + + return backgroundView + }() + + // MARK: - Public methods + func prepareForFadeAnimation() { + self.backgroundView.layer.opacity = 0.0 + self.backgroundColor = UIColor.grayColor().colorWithAlphaComponent(0.65) //TODO: Let user choose color. + } + + + func reset() { + self.coachMark = nil + self.coachMarkView = nil + + self.backgroundView.layer.opacity = 1.0 + self.backgroundColor = UIColor.clearColor() + } + + func respondtoChangeWithNewCoachMark() { + + } + + func addCoachMarkView(coachMarkView: CoachMarkView?) { + guard coachMarkView = coachMarkView else { + return + } + + guard let coachView = newCoachMarkView else { + return + } + + self.addSubview(coachView) + + self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(10)-[coachView]-(10)-|", options: NSLayoutFormatOptions(rawValue: 0), + metrics: nil, views: ["coachView": coachView])) + + self.hintViewFromTopConstraint = NSLayoutConstraint(item: coachView, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0) + self.hintViewFromBottomConstraint = NSLayoutConstraint(item: coachView, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: 0) + + self.addConstraint(self.hintViewFromTopConstraint!) + self.addConstraint(self.hintViewFromBottomConstraint!) + + self.hintViewFromTopConstraint!.active = false + self.hintViewFromBottomConstraint!.active = true + } + + + + // - Private methods + private func prepareForDisplay() { + guard let coachMark = self.privateCoachMark else { + return + } + + if (coachMark.arrowCenterXPosition != nil) { + self.coachMarkView?.changearrowOrientationTo(self.center.x - coachMark.arrowCenterXPosition!) + } + + if let cutoutPath = coachMark.cutoutPath { + if cutoutPath.bounds.origin.y > self.backgroundView.center.y { + self.hintViewFromBottomConstraint?.constant = -(self.backgroundView.frame.size.height - cutoutPath.bounds.origin.y + 2) + + self.hintViewFromBottomConstraint?.active = true + self.hintViewFromTopConstraint?.active = false + } else { + self.hintViewFromTopConstraint?.constant = (cutoutPath.bounds.origin.y + cutoutPath.bounds.size.height) + 2 + + self.hintViewFromBottomConstraint?.active = false + self.hintViewFromTopConstraint?.active = true + } + } else { + + } + + self.setNeedsUpdateConstraints() + } +} \ No newline at end of file diff --git a/Source/OverlayViewDelegate.swift b/Source/OverlayViewDelegate.swift new file mode 100644 index 00000000..d55b7799 --- /dev/null +++ b/Source/OverlayViewDelegate.swift @@ -0,0 +1,31 @@ +// OverlayViewDelegate.swift +// +// Copyright (c) 2015 Frédéric Maquin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// This protocol expected to be implemented by CoachMarkManager, so +/// it can be notified when a tap occured on the overlay. +internal protocol OverlayViewDelegate: class { + + /// Called when the overlay received a tap event. + func didReceivedSingleTap() +} \ No newline at end of file diff --git a/Tests/InstructionsTests.swift b/Tests/InstructionsTests.swift index 6c085427..e8a3ff22 100644 --- a/Tests/InstructionsTests.swift +++ b/Tests/InstructionsTests.swift @@ -1,10 +1,24 @@ +// InstructionsTests.swift // -// InstructionsTests.swift -// InstructionsTests +// Copyright (c) 2015 Frédéric Maquin // -// Created by Frédéric Maquin on 26/09/15. -// Copyright © 2015 Ephread. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: // +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import XCTest @testable import Instructions