├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CHANGELOG.md ├── Example ├── Podfile ├── Podfile.lock ├── Pods │ ├── Local Podspecs │ │ └── TipSee.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ │ └── project.pbxproj │ └── Target Support Files │ │ ├── Pods-TipSee_Example │ │ ├── Pods-TipSee_Example-Info.plist │ │ ├── Pods-TipSee_Example-acknowledgements.markdown │ │ ├── Pods-TipSee_Example-acknowledgements.plist │ │ ├── Pods-TipSee_Example-dummy.m │ │ ├── Pods-TipSee_Example-frameworks.sh │ │ ├── Pods-TipSee_Example-umbrella.h │ │ ├── Pods-TipSee_Example.debug.xcconfig │ │ ├── Pods-TipSee_Example.modulemap │ │ └── Pods-TipSee_Example.release.xcconfig │ │ ├── Pods-TipSee_Tests │ │ ├── Pods-TipSee_Tests-Info.plist │ │ ├── Pods-TipSee_Tests-acknowledgements.markdown │ │ ├── Pods-TipSee_Tests-acknowledgements.plist │ │ ├── Pods-TipSee_Tests-dummy.m │ │ ├── Pods-TipSee_Tests-umbrella.h │ │ ├── Pods-TipSee_Tests.debug.xcconfig │ │ ├── Pods-TipSee_Tests.modulemap │ │ └── Pods-TipSee_Tests.release.xcconfig │ │ └── TipSee │ │ ├── TipSee-Info.plist │ │ ├── TipSee-dummy.m │ │ ├── TipSee-prefix.pch │ │ ├── TipSee-umbrella.h │ │ ├── TipSee.debug.xcconfig │ │ ├── TipSee.modulemap │ │ ├── TipSee.release.xcconfig │ │ └── TipSee.xcconfig ├── Tests │ └── Info.plist ├── TipSee.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── TipSee-Example.xcscheme ├── TipSee.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── TipSee │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── Images.xcassets │ ├── 978146509981.imageset │ │ ├── 978146509981.jpg │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── bottom.imageset │ │ ├── Contents.json │ │ └── Screen Shot 2019-10-31 at 11.03.32 PM.png │ ├── dog.imageset │ │ ├── Contents.json │ │ └── Screen Shot 2019-10-31 at 11.00.25 PM.png │ ├── edit.imageset │ │ ├── 001-pencil.png │ │ └── Contents.json │ ├── eggs.imageset │ │ ├── Contents.json │ │ └── Screen Shot 2019-11-01 at 9.59.07 AM.png │ ├── fashion-profile_2x.imageset │ │ ├── Contents.json │ │ └── fashion-profile_2x.png │ ├── galaxy.imageset │ │ ├── Contents.json │ │ └── realistic-galaxy-background_52683-12122.jpg │ ├── heart-like.imageset │ │ ├── Contents.json │ │ └── heart-like.png │ ├── left-arrow.imageset │ │ ├── Contents.json │ │ └── left-arrow.pdf │ ├── moon.imageset │ │ ├── Contents.json │ │ └── moon-icon-flat-01-.jpg │ ├── photo.imageset │ │ ├── 002-add.png │ │ └── Contents.json │ ├── profile.imageset │ │ ├── Contents.json │ │ └── timothy-paul-smith-256424-1200x800.jpg │ ├── pug_puppy.imageset │ │ ├── Contents.json │ │ └── pug_puppy.png │ ├── right-arrow.imageset │ │ ├── Contents.json │ │ └── right-arrow.pdf │ └── sunny.imageset │ │ ├── Contents.json │ │ └── sunny-176-781177.png │ ├── Info.plist │ ├── TipSee_Example-Bridging-Header.h │ ├── ViewController.swift │ └── images │ ├── TipSee.gif │ └── logo.jpg ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Assets │ └── .gitkeep └── TipSee │ ├── BubbleView.swift │ ├── NSAttributedString+Size.swift │ ├── TipItem.swift │ ├── TipOption.swift │ ├── TipSee+AttributedText.swift │ ├── TipSee.swift │ ├── TipSeeConfiguration.swift │ ├── TipSeeManager+AttributedText.swift │ ├── TipSeeManager.swift │ ├── TipSeeManagerProtocol.swift │ ├── TipTarget.swift │ └── UIViewController+Exts.swift ├── Tests └── TipSeeTests │ ├── OptionsTests.swift │ ├── TipItemTests.swift │ ├── TipSeeManagerTests.swift │ ├── TipSeeTests.swift │ └── XCTestManifests.swift ├── TipSee.podspec └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | *.DS_Store -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.1.0] - 2019-08-06 11 | 12 | ### Added 13 | 14 | - The whole ToolTip-framework. There is no changelog available prior. 15 | 16 | [Unreleased]: https://git.webdooz.com/iOS/tooltip/compare/1.1.0...master 17 | [1.1.0]: https://git.webdooz.com/iOS/tooltip/tags/1.1.0 18 | 19 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | target 'TipSee_Example' do 4 | pod 'TipSee', :path => '../' 5 | platform :ios, '9.0' 6 | 7 | target 'TipSee_Tests' do 8 | inherit! :search_paths 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - TipSee (1.6.0) 3 | 4 | DEPENDENCIES: 5 | - TipSee (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | TipSee: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | TipSee: 6f42ca15824d18615f1ed332b0f823fa370faa2f 13 | 14 | PODFILE CHECKSUM: b049de74e3242ca8cde72714240715a87a8f0d2a 15 | 16 | COCOAPODS: 1.9.1 17 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/TipSee.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TipSee", 3 | "version": "1.6.0", 4 | "module_name": "TipSee", 5 | "summary": "A lightweight, highly customizable tip / hint library for Swift", 6 | "swift_versions": "5.0", 7 | "description": "TipSee is a lightweight and highly customizable library that helps you to show beautiful tips and hints.", 8 | "homepage": "https://github.com/farshadjahanmanesh/TipSee", 9 | "screenshots": [ 10 | "https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_1.png", 11 | "https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_2.png", 12 | "https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_3.png", 13 | "https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_4.png", 14 | "https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_5.png", 15 | "https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_6.png", 16 | "https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC.gif" 17 | ], 18 | "license": { 19 | "type": "MIT", 20 | "file": "LICENSE" 21 | }, 22 | "authors": { 23 | "farshadjahanmanesh": "farshadjahanmanesh@gmail.com" 24 | }, 25 | "source": { 26 | "git": "https://github.com/farshadjahanmanesh/TipSee", 27 | "tag": "1.6.0" 28 | }, 29 | "social_media_url": "https://twitter.com/", 30 | "platforms": { 31 | "ios": "9.0" 32 | }, 33 | "source_files": "Sources/**/*.swift", 34 | "frameworks": "UIKit", 35 | "swift_version": "5.0" 36 | } 37 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - TipSee (1.6.0) 3 | 4 | DEPENDENCIES: 5 | - TipSee (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | TipSee: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | TipSee: 6f42ca15824d18615f1ed332b0f823fa370faa2f 13 | 14 | PODFILE CHECKSUM: b049de74e3242ca8cde72714240715a87a8f0d2a 15 | 16 | COCOAPODS: 1.9.1 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## TipSee 5 | 6 | Copyright (c) 2019 farshadjahanmanesh 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | Generated by CocoaPods - https://cocoapods.org 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2019 farshadjahanmanesh <farshadjahanmanesh@gmail.com> 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | 37 | License 38 | MIT 39 | Title 40 | TipSee 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Generated by CocoaPods - https://cocoapods.org 47 | Title 48 | 49 | Type 50 | PSGroupSpecifier 51 | 52 | 53 | StringsTable 54 | Acknowledgements 55 | Title 56 | Acknowledgements 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_TipSee_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_TipSee_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | 23 | # Used as a return value for each invocation of `strip_invalid_archs` function. 24 | STRIP_BINARY_RETVAL=0 25 | 26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 29 | 30 | # Copies and strips a vendored framework 31 | install_framework() 32 | { 33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 34 | local source="${BUILT_PRODUCTS_DIR}/$1" 35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 37 | elif [ -r "$1" ]; then 38 | local source="$1" 39 | fi 40 | 41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 42 | 43 | if [ -L "${source}" ]; then 44 | echo "Symlinked..." 45 | source="$(readlink "${source}")" 46 | fi 47 | 48 | # Use filter instead of exclude so missing patterns don't throw errors. 49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 51 | 52 | local basename 53 | basename="$(basename -s .framework "$1")" 54 | binary="${destination}/${basename}.framework/${basename}" 55 | 56 | if ! [ -r "$binary" ]; then 57 | binary="${destination}/${basename}" 58 | elif [ -L "${binary}" ]; then 59 | echo "Destination binary is symlinked..." 60 | dirname="$(dirname "${binary}")" 61 | binary="${dirname}/$(readlink "${binary}")" 62 | fi 63 | 64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 66 | strip_invalid_archs "$binary" 67 | fi 68 | 69 | # Resign the code if required by the build settings to avoid unstable apps 70 | code_sign_if_enabled "${destination}/$(basename "$1")" 71 | 72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 74 | local swift_runtime_libs 75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 76 | for lib in $swift_runtime_libs; do 77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 79 | code_sign_if_enabled "${destination}/${lib}" 80 | done 81 | fi 82 | } 83 | 84 | # Copies and strips a vendored dSYM 85 | install_dsym() { 86 | local source="$1" 87 | warn_missing_arch=${2:-true} 88 | if [ -r "$source" ]; then 89 | # Copy the dSYM into the targets temp dir. 90 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 91 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 92 | 93 | local basename 94 | basename="$(basename -s .dSYM "$source")" 95 | binary_name="$(ls "$source/Contents/Resources/DWARF")" 96 | binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" 97 | 98 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 99 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 100 | strip_invalid_archs "$binary" "$warn_missing_arch" 101 | fi 102 | 103 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 104 | # Move the stripped file into its final destination. 105 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 106 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 107 | else 108 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 109 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" 110 | fi 111 | fi 112 | } 113 | 114 | # Copies the bcsymbolmap files of a vendored framework 115 | install_bcsymbolmap() { 116 | local bcsymbolmap_path="$1" 117 | local destination="${BUILT_PRODUCTS_DIR}" 118 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 119 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 120 | } 121 | 122 | # Signs a framework with the provided identity 123 | code_sign_if_enabled() { 124 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 125 | # Use the current code_sign_identity 126 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 127 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 128 | 129 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 130 | code_sign_cmd="$code_sign_cmd &" 131 | fi 132 | echo "$code_sign_cmd" 133 | eval "$code_sign_cmd" 134 | fi 135 | } 136 | 137 | # Strip invalid architectures 138 | strip_invalid_archs() { 139 | binary="$1" 140 | warn_missing_arch=${2:-true} 141 | # Get architectures for current target binary 142 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 143 | # Intersect them with the architectures we are building for 144 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 145 | # If there are no archs supported by this binary then warn the user 146 | if [[ -z "$intersected_archs" ]]; then 147 | if [[ "$warn_missing_arch" == "true" ]]; then 148 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 149 | fi 150 | STRIP_BINARY_RETVAL=0 151 | return 152 | fi 153 | stripped="" 154 | for arch in $binary_archs; do 155 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 156 | # Strip non-valid architectures in-place 157 | lipo -remove "$arch" -output "$binary" "$binary" 158 | stripped="$stripped $arch" 159 | fi 160 | done 161 | if [[ "$stripped" ]]; then 162 | echo "Stripped $binary of architectures:$stripped" 163 | fi 164 | STRIP_BINARY_RETVAL=1 165 | } 166 | 167 | install_artifact() { 168 | artifact="$1" 169 | base="$(basename "$artifact")" 170 | case $base in 171 | *.framework) 172 | install_framework "$artifact" 173 | ;; 174 | *.dSYM) 175 | # Suppress arch warnings since XCFrameworks will include many dSYM files 176 | install_dsym "$artifact" "false" 177 | ;; 178 | *.bcsymbolmap) 179 | install_bcsymbolmap "$artifact" 180 | ;; 181 | *) 182 | echo "error: Unrecognized artifact "$artifact"" 183 | ;; 184 | esac 185 | } 186 | 187 | copy_artifacts() { 188 | file_list="$1" 189 | while read artifact; do 190 | install_artifact "$artifact" 191 | done <$file_list 192 | } 193 | 194 | ARTIFACT_LIST_FILE="${BUILT_PRODUCTS_DIR}/cocoapods-artifacts-${CONFIGURATION}.txt" 195 | if [ -r "${ARTIFACT_LIST_FILE}" ]; then 196 | copy_artifacts "${ARTIFACT_LIST_FILE}" 197 | fi 198 | 199 | if [[ "$CONFIGURATION" == "Debug" ]]; then 200 | install_framework "${BUILT_PRODUCTS_DIR}/TipSee/TipSee.framework" 201 | fi 202 | if [[ "$CONFIGURATION" == "Release" ]]; then 203 | install_framework "${BUILT_PRODUCTS_DIR}/TipSee/TipSee.framework" 204 | fi 205 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 206 | wait 207 | fi 208 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_TipSee_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_TipSee_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/TipSee" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/TipSee/TipSee.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "TipSee" -framework "UIKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_TipSee_Example { 2 | umbrella header "Pods-TipSee_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/TipSee" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/TipSee/TipSee.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "TipSee" -framework "UIKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_TipSee_Tests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_TipSee_Tests 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_TipSee_TestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_TipSee_TestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/TipSee" 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/TipSee/TipSee.framework/Headers" 4 | OTHER_LDFLAGS = $(inherited) -framework "TipSee" -framework "UIKit" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 8 | PODS_ROOT = ${SRCROOT}/Pods 9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 10 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_TipSee_Tests { 2 | umbrella header "Pods-TipSee_Tests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests.release.xcconfig: -------------------------------------------------------------------------------- 1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/TipSee" 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/TipSee/TipSee.framework/Headers" 4 | OTHER_LDFLAGS = $(inherited) -framework "TipSee" -framework "UIKit" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 8 | PODS_ROOT = ${SRCROOT}/Pods 9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 10 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/TipSee/TipSee-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.6.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/TipSee/TipSee-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_TipSee : NSObject 3 | @end 4 | @implementation PodsDummy_TipSee 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/TipSee/TipSee-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/TipSee/TipSee-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double TipSeeVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char TipSeeVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/TipSee/TipSee.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/TipSee 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_LDFLAGS = $(inherited) -framework "UIKit" 4 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/TipSee/TipSee.modulemap: -------------------------------------------------------------------------------- 1 | framework module TipSee { 2 | umbrella header "TipSee-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/TipSee/TipSee.release.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/TipSee 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_LDFLAGS = $(inherited) -framework "UIKit" 4 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/TipSee/TipSee.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/TipSee 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/TipSee.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 53AF54132438B78E00B26705 /* OptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53AF54092438B78000B26705 /* OptionsTests.swift */; }; 11 | 53AF54142438B78E00B26705 /* TipSeeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53AF540A2438B78000B26705 /* TipSeeTests.swift */; }; 12 | 53AF54152438B78E00B26705 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53AF540B2438B78000B26705 /* XCTestManifests.swift */; }; 13 | 53AF54162438B78E00B26705 /* TipItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53AF540C2438B78000B26705 /* TipItemTests.swift */; }; 14 | 53AF54172438B78E00B26705 /* TipSeeManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53AF540D2438B78000B26705 /* TipSeeManagerTests.swift */; }; 15 | 5AC6E7D42FDCDF1B8F12B48B /* Pods_TipSee_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E79AABA03E0B1320CB887CE /* Pods_TipSee_Tests.framework */; }; 16 | 87BCBA3D2358E166000BCF46 /* TipSee.gif in Resources */ = {isa = PBXBuildFile; fileRef = 87BCBA352358E166000BCF46 /* TipSee.gif */; }; 17 | 87BCBA442358E166000BCF46 /* logo.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 87BCBA3C2358E166000BCF46 /* logo.jpg */; }; 18 | 87BCBA462358E202000BCF46 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 87BCBA452358E202000BCF46 /* Images.xcassets */; }; 19 | 87BCBA492358E212000BCF46 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BCBA482358E211000BCF46 /* AppDelegate.swift */; }; 20 | 87BCBA502358E21D000BCF46 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BCBA4A2358E21D000BCF46 /* ViewController.swift */; }; 21 | 87BCBA512358E21D000BCF46 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 87BCBA4C2358E21D000BCF46 /* LaunchScreen.xib */; }; 22 | 87BCBA522358E21D000BCF46 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 87BCBA4E2358E21D000BCF46 /* Main.storyboard */; }; 23 | 87BCBA582358E3FB000BCF46 /* TipSee.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 87BCBA532358E3FB000BCF46 /* TipSee.podspec */; }; 24 | 87BCBA592358E3FB000BCF46 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 87BCBA542358E3FB000BCF46 /* LICENSE */; }; 25 | 87BCBA5A2358E3FB000BCF46 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = 87BCBA552358E3FB000BCF46 /* CHANGELOG.md */; }; 26 | 87BCBA5C2358E3FB000BCF46 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 87BCBA572358E3FB000BCF46 /* README.md */; }; 27 | A5DA80CC545C67E4D77AE5EF /* Pods_TipSee_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BBADEF3899A7E55B834BF071 /* Pods_TipSee_Example.framework */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXContainerItemProxy section */ 31 | 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = 607FACC81AFB9204008FA782 /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = 607FACCF1AFB9204008FA782; 36 | remoteInfo = TipSee; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 0E79AABA03E0B1320CB887CE /* Pods_TipSee_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TipSee_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 185AE1F4B18EDFA276B12D07 /* Pods-TipSee_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TipSee_Example.debug.xcconfig"; path = "Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example.debug.xcconfig"; sourceTree = ""; }; 43 | 1E84A910905483281EED216C /* Pods-TipSee_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TipSee_Tests.debug.xcconfig"; path = "Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests.debug.xcconfig"; sourceTree = ""; }; 44 | 53AF54092438B78000B26705 /* OptionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionsTests.swift; sourceTree = ""; }; 45 | 53AF540A2438B78000B26705 /* TipSeeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TipSeeTests.swift; sourceTree = ""; }; 46 | 53AF540B2438B78000B26705 /* XCTestManifests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = ""; }; 47 | 53AF540C2438B78000B26705 /* TipItemTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TipItemTests.swift; sourceTree = ""; }; 48 | 53AF540D2438B78000B26705 /* TipSeeManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TipSeeManagerTests.swift; sourceTree = ""; }; 49 | 607FACD01AFB9204008FA782 /* TipSee_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TipSee_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 607FACE51AFB9204008FA782 /* TipSee_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TipSee_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | 6E4A28C99435F4EA34213DE2 /* Pods-TipSee_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TipSee_Tests.release.xcconfig"; path = "Target Support Files/Pods-TipSee_Tests/Pods-TipSee_Tests.release.xcconfig"; sourceTree = ""; }; 54 | 87BCBA352358E166000BCF46 /* TipSee.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = TipSee.gif; sourceTree = ""; }; 55 | 87BCBA3C2358E166000BCF46 /* logo.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = logo.jpg; sourceTree = ""; }; 56 | 87BCBA452358E202000BCF46 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 57 | 87BCBA472358E211000BCF46 /* TipSee_Example-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TipSee_Example-Bridging-Header.h"; sourceTree = ""; }; 58 | 87BCBA482358E211000BCF46 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 59 | 87BCBA4A2358E21D000BCF46 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 60 | 87BCBA4D2358E21D000BCF46 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = LaunchScreen.xib; sourceTree = ""; }; 61 | 87BCBA4F2358E21D000BCF46 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Main.storyboard; sourceTree = ""; }; 62 | 87BCBA532358E3FB000BCF46 /* TipSee.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = TipSee.podspec; path = ../TipSee.podspec; sourceTree = ""; }; 63 | 87BCBA542358E3FB000BCF46 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 64 | 87BCBA552358E3FB000BCF46 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; 65 | 87BCBA572358E3FB000BCF46 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 66 | A41F56FC353C7EC3C9F6E995 /* Pods-TipSee_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TipSee_Example.release.xcconfig"; path = "Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example.release.xcconfig"; sourceTree = ""; }; 67 | BBADEF3899A7E55B834BF071 /* Pods_TipSee_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TipSee_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | /* End PBXFileReference section */ 69 | 70 | /* Begin PBXFrameworksBuildPhase section */ 71 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | A5DA80CC545C67E4D77AE5EF /* Pods_TipSee_Example.framework in Frameworks */, 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | 607FACE21AFB9204008FA782 /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | 5AC6E7D42FDCDF1B8F12B48B /* Pods_TipSee_Tests.framework in Frameworks */, 84 | ); 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | /* End PBXFrameworksBuildPhase section */ 88 | 89 | /* Begin PBXGroup section */ 90 | 53AF54082438B78000B26705 /* TipSeeTests */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 53AF54092438B78000B26705 /* OptionsTests.swift */, 94 | 53AF540A2438B78000B26705 /* TipSeeTests.swift */, 95 | 53AF540B2438B78000B26705 /* XCTestManifests.swift */, 96 | 53AF540C2438B78000B26705 /* TipItemTests.swift */, 97 | 53AF540D2438B78000B26705 /* TipSeeManagerTests.swift */, 98 | ); 99 | name = TipSeeTests; 100 | path = ../../Tests/TipSeeTests; 101 | sourceTree = ""; 102 | }; 103 | 607FACC71AFB9204008FA782 = { 104 | isa = PBXGroup; 105 | children = ( 106 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 107 | 607FACD21AFB9204008FA782 /* Example for TipSee */, 108 | 607FACE81AFB9204008FA782 /* Tests */, 109 | 607FACD11AFB9204008FA782 /* Products */, 110 | BA030862CC1C4DE09AAB79A1 /* Pods */, 111 | C906ACBFC6B38C291DC1BEF7 /* Frameworks */, 112 | ); 113 | sourceTree = ""; 114 | }; 115 | 607FACD11AFB9204008FA782 /* Products */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 607FACD01AFB9204008FA782 /* TipSee_Example.app */, 119 | 607FACE51AFB9204008FA782 /* TipSee_Tests.xctest */, 120 | ); 121 | name = Products; 122 | sourceTree = ""; 123 | }; 124 | 607FACD21AFB9204008FA782 /* Example for TipSee */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 87BCBA482358E211000BCF46 /* AppDelegate.swift */, 128 | 87BCBA4B2358E21D000BCF46 /* Base.lproj */, 129 | 87BCBA4A2358E21D000BCF46 /* ViewController.swift */, 130 | 87BCBA452358E202000BCF46 /* Images.xcassets */, 131 | 87BCBA342358E166000BCF46 /* images */, 132 | 607FACD31AFB9204008FA782 /* Supporting Files */, 133 | 87BCBA472358E211000BCF46 /* TipSee_Example-Bridging-Header.h */, 134 | ); 135 | name = "Example for TipSee"; 136 | path = TipSee; 137 | sourceTree = ""; 138 | }; 139 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 607FACD41AFB9204008FA782 /* Info.plist */, 143 | ); 144 | name = "Supporting Files"; 145 | sourceTree = ""; 146 | }; 147 | 607FACE81AFB9204008FA782 /* Tests */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 53AF54082438B78000B26705 /* TipSeeTests */, 151 | 607FACE91AFB9204008FA782 /* Supporting Files */, 152 | ); 153 | path = Tests; 154 | sourceTree = ""; 155 | }; 156 | 607FACE91AFB9204008FA782 /* Supporting Files */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 607FACEA1AFB9204008FA782 /* Info.plist */, 160 | ); 161 | name = "Supporting Files"; 162 | sourceTree = ""; 163 | }; 164 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 87BCBA552358E3FB000BCF46 /* CHANGELOG.md */, 168 | 87BCBA542358E3FB000BCF46 /* LICENSE */, 169 | 87BCBA572358E3FB000BCF46 /* README.md */, 170 | 87BCBA532358E3FB000BCF46 /* TipSee.podspec */, 171 | ); 172 | name = "Podspec Metadata"; 173 | sourceTree = ""; 174 | }; 175 | 87BCBA342358E166000BCF46 /* images */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | 87BCBA352358E166000BCF46 /* TipSee.gif */, 179 | 87BCBA3C2358E166000BCF46 /* logo.jpg */, 180 | ); 181 | path = images; 182 | sourceTree = ""; 183 | }; 184 | 87BCBA4B2358E21D000BCF46 /* Base.lproj */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 87BCBA4C2358E21D000BCF46 /* LaunchScreen.xib */, 188 | 87BCBA4E2358E21D000BCF46 /* Main.storyboard */, 189 | ); 190 | path = Base.lproj; 191 | sourceTree = ""; 192 | }; 193 | BA030862CC1C4DE09AAB79A1 /* Pods */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 185AE1F4B18EDFA276B12D07 /* Pods-TipSee_Example.debug.xcconfig */, 197 | A41F56FC353C7EC3C9F6E995 /* Pods-TipSee_Example.release.xcconfig */, 198 | 1E84A910905483281EED216C /* Pods-TipSee_Tests.debug.xcconfig */, 199 | 6E4A28C99435F4EA34213DE2 /* Pods-TipSee_Tests.release.xcconfig */, 200 | ); 201 | path = Pods; 202 | sourceTree = ""; 203 | }; 204 | C906ACBFC6B38C291DC1BEF7 /* Frameworks */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | BBADEF3899A7E55B834BF071 /* Pods_TipSee_Example.framework */, 208 | 0E79AABA03E0B1320CB887CE /* Pods_TipSee_Tests.framework */, 209 | ); 210 | name = Frameworks; 211 | sourceTree = ""; 212 | }; 213 | /* End PBXGroup section */ 214 | 215 | /* Begin PBXNativeTarget section */ 216 | 607FACCF1AFB9204008FA782 /* TipSee_Example */ = { 217 | isa = PBXNativeTarget; 218 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "TipSee_Example" */; 219 | buildPhases = ( 220 | DD5D0DA56B7CF940AD85E7B2 /* [CP] Check Pods Manifest.lock */, 221 | 607FACCC1AFB9204008FA782 /* Sources */, 222 | 607FACCD1AFB9204008FA782 /* Frameworks */, 223 | 607FACCE1AFB9204008FA782 /* Resources */, 224 | 44F0E925B2C39F9A7BEC72F9 /* [CP] Embed Pods Frameworks */, 225 | ); 226 | buildRules = ( 227 | ); 228 | dependencies = ( 229 | ); 230 | name = TipSee_Example; 231 | productName = TipSee; 232 | productReference = 607FACD01AFB9204008FA782 /* TipSee_Example.app */; 233 | productType = "com.apple.product-type.application"; 234 | }; 235 | 607FACE41AFB9204008FA782 /* TipSee_Tests */ = { 236 | isa = PBXNativeTarget; 237 | buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "TipSee_Tests" */; 238 | buildPhases = ( 239 | 69217D45001E578D2FADAED2 /* [CP] Check Pods Manifest.lock */, 240 | 607FACE11AFB9204008FA782 /* Sources */, 241 | 607FACE21AFB9204008FA782 /* Frameworks */, 242 | 607FACE31AFB9204008FA782 /* Resources */, 243 | ); 244 | buildRules = ( 245 | ); 246 | dependencies = ( 247 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */, 248 | ); 249 | name = TipSee_Tests; 250 | productName = Tests; 251 | productReference = 607FACE51AFB9204008FA782 /* TipSee_Tests.xctest */; 252 | productType = "com.apple.product-type.bundle.unit-test"; 253 | }; 254 | /* End PBXNativeTarget section */ 255 | 256 | /* Begin PBXProject section */ 257 | 607FACC81AFB9204008FA782 /* Project object */ = { 258 | isa = PBXProject; 259 | attributes = { 260 | LastSwiftUpdateCheck = 1110; 261 | LastUpgradeCheck = 1110; 262 | ORGANIZATIONNAME = CocoaPods; 263 | TargetAttributes = { 264 | 607FACCF1AFB9204008FA782 = { 265 | CreatedOnToolsVersion = 6.3.1; 266 | LastSwiftMigration = 1110; 267 | }; 268 | 607FACE41AFB9204008FA782 = { 269 | CreatedOnToolsVersion = 6.3.1; 270 | LastSwiftMigration = 1110; 271 | TestTargetID = 607FACCF1AFB9204008FA782; 272 | }; 273 | }; 274 | }; 275 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "TipSee" */; 276 | compatibilityVersion = "Xcode 3.2"; 277 | developmentRegion = en; 278 | hasScannedForEncodings = 0; 279 | knownRegions = ( 280 | en, 281 | Base, 282 | ); 283 | mainGroup = 607FACC71AFB9204008FA782; 284 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 285 | projectDirPath = ""; 286 | projectRoot = ""; 287 | targets = ( 288 | 607FACCF1AFB9204008FA782 /* TipSee_Example */, 289 | 607FACE41AFB9204008FA782 /* TipSee_Tests */, 290 | ); 291 | }; 292 | /* End PBXProject section */ 293 | 294 | /* Begin PBXResourcesBuildPhase section */ 295 | 607FACCE1AFB9204008FA782 /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | 87BCBA512358E21D000BCF46 /* LaunchScreen.xib in Resources */, 300 | 87BCBA462358E202000BCF46 /* Images.xcassets in Resources */, 301 | 87BCBA592358E3FB000BCF46 /* LICENSE in Resources */, 302 | 87BCBA5A2358E3FB000BCF46 /* CHANGELOG.md in Resources */, 303 | 87BCBA5C2358E3FB000BCF46 /* README.md in Resources */, 304 | 87BCBA442358E166000BCF46 /* logo.jpg in Resources */, 305 | 87BCBA522358E21D000BCF46 /* Main.storyboard in Resources */, 306 | 87BCBA3D2358E166000BCF46 /* TipSee.gif in Resources */, 307 | 87BCBA582358E3FB000BCF46 /* TipSee.podspec in Resources */, 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | 607FACE31AFB9204008FA782 /* Resources */ = { 312 | isa = PBXResourcesBuildPhase; 313 | buildActionMask = 2147483647; 314 | files = ( 315 | ); 316 | runOnlyForDeploymentPostprocessing = 0; 317 | }; 318 | /* End PBXResourcesBuildPhase section */ 319 | 320 | /* Begin PBXShellScriptBuildPhase section */ 321 | 44F0E925B2C39F9A7BEC72F9 /* [CP] Embed Pods Frameworks */ = { 322 | isa = PBXShellScriptBuildPhase; 323 | buildActionMask = 2147483647; 324 | files = ( 325 | ); 326 | inputPaths = ( 327 | "${PODS_ROOT}/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example-frameworks.sh", 328 | "${BUILT_PRODUCTS_DIR}/TipSee/TipSee.framework", 329 | ); 330 | name = "[CP] Embed Pods Frameworks"; 331 | outputPaths = ( 332 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TipSee.framework", 333 | ); 334 | runOnlyForDeploymentPostprocessing = 0; 335 | shellPath = /bin/sh; 336 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-TipSee_Example/Pods-TipSee_Example-frameworks.sh\"\n"; 337 | showEnvVarsInLog = 0; 338 | }; 339 | 69217D45001E578D2FADAED2 /* [CP] Check Pods Manifest.lock */ = { 340 | isa = PBXShellScriptBuildPhase; 341 | buildActionMask = 2147483647; 342 | files = ( 343 | ); 344 | inputFileListPaths = ( 345 | ); 346 | inputPaths = ( 347 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 348 | "${PODS_ROOT}/Manifest.lock", 349 | ); 350 | name = "[CP] Check Pods Manifest.lock"; 351 | outputFileListPaths = ( 352 | ); 353 | outputPaths = ( 354 | "$(DERIVED_FILE_DIR)/Pods-TipSee_Tests-checkManifestLockResult.txt", 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | shellPath = /bin/sh; 358 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 359 | showEnvVarsInLog = 0; 360 | }; 361 | DD5D0DA56B7CF940AD85E7B2 /* [CP] Check Pods Manifest.lock */ = { 362 | isa = PBXShellScriptBuildPhase; 363 | buildActionMask = 2147483647; 364 | files = ( 365 | ); 366 | inputFileListPaths = ( 367 | ); 368 | inputPaths = ( 369 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 370 | "${PODS_ROOT}/Manifest.lock", 371 | ); 372 | name = "[CP] Check Pods Manifest.lock"; 373 | outputFileListPaths = ( 374 | ); 375 | outputPaths = ( 376 | "$(DERIVED_FILE_DIR)/Pods-TipSee_Example-checkManifestLockResult.txt", 377 | ); 378 | runOnlyForDeploymentPostprocessing = 0; 379 | shellPath = /bin/sh; 380 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 381 | showEnvVarsInLog = 0; 382 | }; 383 | /* End PBXShellScriptBuildPhase section */ 384 | 385 | /* Begin PBXSourcesBuildPhase section */ 386 | 607FACCC1AFB9204008FA782 /* Sources */ = { 387 | isa = PBXSourcesBuildPhase; 388 | buildActionMask = 2147483647; 389 | files = ( 390 | 87BCBA502358E21D000BCF46 /* ViewController.swift in Sources */, 391 | 87BCBA492358E212000BCF46 /* AppDelegate.swift in Sources */, 392 | ); 393 | runOnlyForDeploymentPostprocessing = 0; 394 | }; 395 | 607FACE11AFB9204008FA782 /* Sources */ = { 396 | isa = PBXSourcesBuildPhase; 397 | buildActionMask = 2147483647; 398 | files = ( 399 | 53AF54152438B78E00B26705 /* XCTestManifests.swift in Sources */, 400 | 53AF54142438B78E00B26705 /* TipSeeTests.swift in Sources */, 401 | 53AF54172438B78E00B26705 /* TipSeeManagerTests.swift in Sources */, 402 | 53AF54162438B78E00B26705 /* TipItemTests.swift in Sources */, 403 | 53AF54132438B78E00B26705 /* OptionsTests.swift in Sources */, 404 | ); 405 | runOnlyForDeploymentPostprocessing = 0; 406 | }; 407 | /* End PBXSourcesBuildPhase section */ 408 | 409 | /* Begin PBXTargetDependency section */ 410 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { 411 | isa = PBXTargetDependency; 412 | target = 607FACCF1AFB9204008FA782 /* TipSee_Example */; 413 | targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; 414 | }; 415 | /* End PBXTargetDependency section */ 416 | 417 | /* Begin PBXVariantGroup section */ 418 | 87BCBA4C2358E21D000BCF46 /* LaunchScreen.xib */ = { 419 | isa = PBXVariantGroup; 420 | children = ( 421 | 87BCBA4D2358E21D000BCF46 /* Base */, 422 | ); 423 | name = LaunchScreen.xib; 424 | sourceTree = ""; 425 | }; 426 | 87BCBA4E2358E21D000BCF46 /* Main.storyboard */ = { 427 | isa = PBXVariantGroup; 428 | children = ( 429 | 87BCBA4F2358E21D000BCF46 /* Base */, 430 | ); 431 | name = Main.storyboard; 432 | sourceTree = ""; 433 | }; 434 | /* End PBXVariantGroup section */ 435 | 436 | /* Begin XCBuildConfiguration section */ 437 | 607FACED1AFB9204008FA782 /* Debug */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | ALWAYS_SEARCH_USER_PATHS = NO; 441 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 442 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 443 | CLANG_CXX_LIBRARY = "libc++"; 444 | CLANG_ENABLE_MODULES = YES; 445 | CLANG_ENABLE_OBJC_ARC = YES; 446 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 447 | CLANG_WARN_BOOL_CONVERSION = YES; 448 | CLANG_WARN_COMMA = YES; 449 | CLANG_WARN_CONSTANT_CONVERSION = YES; 450 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 451 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 452 | CLANG_WARN_EMPTY_BODY = YES; 453 | CLANG_WARN_ENUM_CONVERSION = YES; 454 | CLANG_WARN_INFINITE_RECURSION = YES; 455 | CLANG_WARN_INT_CONVERSION = YES; 456 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 457 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 458 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 459 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 460 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 461 | CLANG_WARN_STRICT_PROTOTYPES = YES; 462 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 463 | CLANG_WARN_UNREACHABLE_CODE = YES; 464 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 465 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 466 | COPY_PHASE_STRIP = NO; 467 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 468 | ENABLE_STRICT_OBJC_MSGSEND = YES; 469 | ENABLE_TESTABILITY = YES; 470 | GCC_C_LANGUAGE_STANDARD = gnu99; 471 | GCC_DYNAMIC_NO_PIC = NO; 472 | GCC_NO_COMMON_BLOCKS = YES; 473 | GCC_OPTIMIZATION_LEVEL = 0; 474 | GCC_PREPROCESSOR_DEFINITIONS = ( 475 | "DEBUG=1", 476 | "$(inherited)", 477 | ); 478 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 479 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 480 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 481 | GCC_WARN_UNDECLARED_SELECTOR = YES; 482 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 483 | GCC_WARN_UNUSED_FUNCTION = YES; 484 | GCC_WARN_UNUSED_VARIABLE = YES; 485 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 486 | MTL_ENABLE_DEBUG_INFO = YES; 487 | ONLY_ACTIVE_ARCH = YES; 488 | SDKROOT = iphoneos; 489 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 490 | }; 491 | name = Debug; 492 | }; 493 | 607FACEE1AFB9204008FA782 /* Release */ = { 494 | isa = XCBuildConfiguration; 495 | buildSettings = { 496 | ALWAYS_SEARCH_USER_PATHS = NO; 497 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 498 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 499 | CLANG_CXX_LIBRARY = "libc++"; 500 | CLANG_ENABLE_MODULES = YES; 501 | CLANG_ENABLE_OBJC_ARC = YES; 502 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 503 | CLANG_WARN_BOOL_CONVERSION = YES; 504 | CLANG_WARN_COMMA = YES; 505 | CLANG_WARN_CONSTANT_CONVERSION = YES; 506 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 507 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 508 | CLANG_WARN_EMPTY_BODY = YES; 509 | CLANG_WARN_ENUM_CONVERSION = YES; 510 | CLANG_WARN_INFINITE_RECURSION = YES; 511 | CLANG_WARN_INT_CONVERSION = YES; 512 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 513 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 514 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 515 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 516 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 517 | CLANG_WARN_STRICT_PROTOTYPES = YES; 518 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 519 | CLANG_WARN_UNREACHABLE_CODE = YES; 520 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 521 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 522 | COPY_PHASE_STRIP = NO; 523 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 524 | ENABLE_NS_ASSERTIONS = NO; 525 | ENABLE_STRICT_OBJC_MSGSEND = YES; 526 | GCC_C_LANGUAGE_STANDARD = gnu99; 527 | GCC_NO_COMMON_BLOCKS = YES; 528 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 529 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 530 | GCC_WARN_UNDECLARED_SELECTOR = YES; 531 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 532 | GCC_WARN_UNUSED_FUNCTION = YES; 533 | GCC_WARN_UNUSED_VARIABLE = YES; 534 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 535 | MTL_ENABLE_DEBUG_INFO = NO; 536 | SDKROOT = iphoneos; 537 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 538 | VALIDATE_PRODUCT = YES; 539 | }; 540 | name = Release; 541 | }; 542 | 607FACF01AFB9204008FA782 /* Debug */ = { 543 | isa = XCBuildConfiguration; 544 | baseConfigurationReference = 185AE1F4B18EDFA276B12D07 /* Pods-TipSee_Example.debug.xcconfig */; 545 | buildSettings = { 546 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 547 | CLANG_ENABLE_MODULES = YES; 548 | INFOPLIST_FILE = TipSee/Info.plist; 549 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 550 | MODULE_NAME = ExampleApp; 551 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 552 | PRODUCT_NAME = "$(TARGET_NAME)"; 553 | SWIFT_OBJC_BRIDGING_HEADER = "TipSee/TipSee_Example-Bridging-Header.h"; 554 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 555 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 556 | SWIFT_VERSION = 5.0; 557 | }; 558 | name = Debug; 559 | }; 560 | 607FACF11AFB9204008FA782 /* Release */ = { 561 | isa = XCBuildConfiguration; 562 | baseConfigurationReference = A41F56FC353C7EC3C9F6E995 /* Pods-TipSee_Example.release.xcconfig */; 563 | buildSettings = { 564 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 565 | CLANG_ENABLE_MODULES = YES; 566 | INFOPLIST_FILE = TipSee/Info.plist; 567 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 568 | MODULE_NAME = ExampleApp; 569 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 570 | PRODUCT_NAME = "$(TARGET_NAME)"; 571 | SWIFT_OBJC_BRIDGING_HEADER = "TipSee/TipSee_Example-Bridging-Header.h"; 572 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 573 | SWIFT_VERSION = 5.0; 574 | }; 575 | name = Release; 576 | }; 577 | 607FACF31AFB9204008FA782 /* Debug */ = { 578 | isa = XCBuildConfiguration; 579 | baseConfigurationReference = 1E84A910905483281EED216C /* Pods-TipSee_Tests.debug.xcconfig */; 580 | buildSettings = { 581 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 582 | FRAMEWORK_SEARCH_PATHS = ( 583 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 584 | "$(inherited)", 585 | ); 586 | GCC_PREPROCESSOR_DEFINITIONS = ( 587 | "DEBUG=1", 588 | "$(inherited)", 589 | ); 590 | INFOPLIST_FILE = Tests/Info.plist; 591 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 592 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 593 | PRODUCT_NAME = "$(TARGET_NAME)"; 594 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 595 | SWIFT_VERSION = 5.0; 596 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TipSee_Example.app/TipSee_Example"; 597 | }; 598 | name = Debug; 599 | }; 600 | 607FACF41AFB9204008FA782 /* Release */ = { 601 | isa = XCBuildConfiguration; 602 | baseConfigurationReference = 6E4A28C99435F4EA34213DE2 /* Pods-TipSee_Tests.release.xcconfig */; 603 | buildSettings = { 604 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 605 | FRAMEWORK_SEARCH_PATHS = ( 606 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 607 | "$(inherited)", 608 | ); 609 | INFOPLIST_FILE = Tests/Info.plist; 610 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 611 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 612 | PRODUCT_NAME = "$(TARGET_NAME)"; 613 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 614 | SWIFT_VERSION = 5.0; 615 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TipSee_Example.app/TipSee_Example"; 616 | }; 617 | name = Release; 618 | }; 619 | /* End XCBuildConfiguration section */ 620 | 621 | /* Begin XCConfigurationList section */ 622 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "TipSee" */ = { 623 | isa = XCConfigurationList; 624 | buildConfigurations = ( 625 | 607FACED1AFB9204008FA782 /* Debug */, 626 | 607FACEE1AFB9204008FA782 /* Release */, 627 | ); 628 | defaultConfigurationIsVisible = 0; 629 | defaultConfigurationName = Release; 630 | }; 631 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "TipSee_Example" */ = { 632 | isa = XCConfigurationList; 633 | buildConfigurations = ( 634 | 607FACF01AFB9204008FA782 /* Debug */, 635 | 607FACF11AFB9204008FA782 /* Release */, 636 | ); 637 | defaultConfigurationIsVisible = 0; 638 | defaultConfigurationName = Release; 639 | }; 640 | 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "TipSee_Tests" */ = { 641 | isa = XCConfigurationList; 642 | buildConfigurations = ( 643 | 607FACF31AFB9204008FA782 /* Debug */, 644 | 607FACF41AFB9204008FA782 /* Release */, 645 | ); 646 | defaultConfigurationIsVisible = 0; 647 | defaultConfigurationName = Release; 648 | }; 649 | /* End XCConfigurationList section */ 650 | }; 651 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 652 | } 653 | -------------------------------------------------------------------------------- /Example/TipSee.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/TipSee.xcodeproj/xcshareddata/xcschemes/TipSee-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 86 | 88 | 94 | 95 | 96 | 97 | 103 | 105 | 111 | 112 | 113 | 114 | 116 | 117 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /Example/TipSee.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/TipSee.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/TipSee/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TipSee 4 | // 5 | // Created by farshadjahanmanesh on 07/16/2019. 6 | // Copyright (c) 2019 farshadjahanmanesh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | UINavigationBar.appearance().barStyle = .black 19 | 20 | return true 21 | } 22 | 23 | func applicationWillResignActive(_ application: UIApplication) { 24 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 25 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 26 | } 27 | 28 | func applicationDidEnterBackground(_ application: UIApplication) { 29 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 31 | } 32 | 33 | func applicationWillEnterForeground(_ application: UIApplication) { 34 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 35 | } 36 | 37 | func applicationDidBecomeActive(_ application: UIApplication) { 38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 39 | } 40 | 41 | func applicationWillTerminate(_ application: UIApplication) { 42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 43 | } 44 | 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Example/TipSee/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Example/TipSee/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 102 | 120 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/978146509981.imageset/978146509981.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/978146509981.imageset/978146509981.jpg -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/978146509981.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "978146509981.jpg" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/bottom.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Screen Shot 2019-10-31 at 11.03.32 PM.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/bottom.imageset/Screen Shot 2019-10-31 at 11.03.32 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/bottom.imageset/Screen Shot 2019-10-31 at 11.03.32 PM.png -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/dog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Screen Shot 2019-10-31 at 11.00.25 PM.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/dog.imageset/Screen Shot 2019-10-31 at 11.00.25 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/dog.imageset/Screen Shot 2019-10-31 at 11.00.25 PM.png -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/edit.imageset/001-pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/edit.imageset/001-pencil.png -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/edit.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "001-pencil.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/eggs.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Screen Shot 2019-11-01 at 9.59.07 AM.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/eggs.imageset/Screen Shot 2019-11-01 at 9.59.07 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/eggs.imageset/Screen Shot 2019-11-01 at 9.59.07 AM.png -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/fashion-profile_2x.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "fashion-profile_2x.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/fashion-profile_2x.imageset/fashion-profile_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/fashion-profile_2x.imageset/fashion-profile_2x.png -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/galaxy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "realistic-galaxy-background_52683-12122.jpg" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/galaxy.imageset/realistic-galaxy-background_52683-12122.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/galaxy.imageset/realistic-galaxy-background_52683-12122.jpg -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/heart-like.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "heart-like.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/heart-like.imageset/heart-like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/heart-like.imageset/heart-like.png -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/left-arrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "left-arrow.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/left-arrow.imageset/left-arrow.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/left-arrow.imageset/left-arrow.pdf -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/moon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "moon-icon-flat-01-.jpg" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/moon.imageset/moon-icon-flat-01-.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/moon.imageset/moon-icon-flat-01-.jpg -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/photo.imageset/002-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/photo.imageset/002-add.png -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/photo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "002-add.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "timothy-paul-smith-256424-1200x800.jpg" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/profile.imageset/timothy-paul-smith-256424-1200x800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/profile.imageset/timothy-paul-smith-256424-1200x800.jpg -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/pug_puppy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pug_puppy.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/pug_puppy.imageset/pug_puppy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/pug_puppy.imageset/pug_puppy.png -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/right-arrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "right-arrow.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/right-arrow.imageset/right-arrow.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/right-arrow.imageset/right-arrow.pdf -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/sunny.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "sunny-176-781177.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/TipSee/Images.xcassets/sunny.imageset/sunny-176-781177.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/Images.xcassets/sunny.imageset/sunny-176-781177.png -------------------------------------------------------------------------------- /Example/TipSee/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIStatusBarStyle 34 | UIStatusBarStyleDarkContent 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | 40 | UIViewControllerBasedStatusBarAppearance 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Example/TipSee/TipSee_Example-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /Example/TipSee/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // TipSee 4 | // 5 | // Created by farshadjahanmanesh on 07/16/2019. 6 | // Copyright (c) 2019 farshadjahanmanesh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TipSee 11 | 12 | class ViewController: UIViewController { 13 | @IBOutlet weak var bigBottomButton : UIButton! 14 | @IBOutlet weak var noConstraintsButton : UIButton! 15 | @IBOutlet weak var transformedButton : UIImageView! 16 | @IBOutlet weak var pugImage : UIView! 17 | @IBOutlet weak var pugName : UIView! 18 | @IBOutlet weak var pugDescrription : UIView! 19 | private var tips : TipSeeManager? 20 | private var rotationDegree : CGFloat = 45 21 | private var startDayAndNight = false 22 | 23 | @IBAction func reShowTips(){ 24 | self.tips?.finish() 25 | self.showTips() 26 | } 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | bigBottomButton.titleLabel?.lineBreakMode = .byWordWrapping 31 | bigBottomButton.titleLabel?.numberOfLines = 0 32 | 33 | if #available(iOS 10.0, *) { 34 | let t = Timer.scheduledTimer(withTimeInterval: 0.75, repeats: true) { [unowned self](_) in 35 | UIView.animate(withDuration: 0.75) { 36 | 37 | if self.startDayAndNight { 38 | if self.rotationDegree < 0 { 39 | // day 40 | self.transformedButton.image = UIImage(named: "sunny") 41 | self.tips?.pointer.options.dimColor = UIColor(red:0.97, green:0.76, blue:0.29, alpha:1.0) 42 | }else{ 43 | // night 44 | self.transformedButton.image = UIImage(named: "moon") 45 | self.tips?.pointer.options.dimColor = UIColor(red:0.02, green:0.18, blue:0.23, alpha:1.0) 46 | } 47 | } 48 | self.transformedButton.transform = self.transformedButton.transform 49 | .rotated(by: self.rotationDegree) 50 | } 51 | } 52 | t.fire() 53 | } 54 | } 55 | 56 | private var pugLoveConfig: TipSee.Options.Bubble { 57 | return TipSee.Options.Bubble 58 | .default() 59 | .with{ 60 | $0.foregroundColor = .white 61 | $0.textAlignments = .justified 62 | $0.position = .top 63 | 64 | } 65 | } 66 | 67 | private var pugDescriptionConfig : TipSee.Options.Bubble { 68 | return TipSee.Options.Bubble 69 | .default() 70 | .with{ 71 | $0.backgroundColor = UIColor.purple 72 | $0.foregroundColor = UIColor.white 73 | $0.position = .left 74 | } 75 | } 76 | 77 | private func showTips() { 78 | 79 | let image = UIImageView(image: #imageLiteral(resourceName: "heart-like.png")) 80 | image.frame = CGRect(x: 0, y: 0, width: 40, height: 40) 81 | image.contentMode = .scaleAspectFit 82 | let transformed = TipSee.Options.Bubble 83 | .default() 84 | .with{ 85 | $0.backgroundColor = UIColor.orange 86 | $0 .foregroundColor = .white 87 | $0 .textAlignments = .left 88 | $0 .position = .right 89 | //$0.changeDimColor = UIColor(red:0.02, green:0.18, blue:0.23, alpha:1.0) 90 | } 91 | 92 | let defaultTipOption = TipSee.Options 93 | .default() 94 | .with { 95 | $0.dimColor = UIColor.black.withAlphaComponent(0.3) 96 | $0.bubbleLiveDuration = .untilNext 97 | $0.dimFading = false 98 | } 99 | 100 | self.tips = TipSeeManager(on: self.view.window!,with: defaultTipOption) 101 | 102 | tips!.add( 103 | new: self.pugImage, 104 | texts: [ 105 | "We can show interactive tips on top of views or anything else conforming to `TipTarget`.", 106 | "The positioning of the tip bubble (or custom view) is automatically calculated by TipSee for you.", 107 | "This decision is based on the size of the tip and the available space around it.", 108 | "Alternatively, the tip position can be explicity set for precise control." 109 | ], 110 | with: pugLoveConfig) 111 | { 112 | previousButton, nextButton in 113 | nextButton.imageView?.contentMode = .scaleAspectFit 114 | previousButton.imageView?.contentMode = .scaleAspectFit 115 | nextButton.setImage(#imageLiteral(resourceName: "right-arrow.pdf"), for: .normal) 116 | previousButton.setImage(#imageLiteral(resourceName: "left-arrow.pdf"), for: .normal) 117 | previousButton.tintColor = .white 118 | nextButton.tintColor = .white 119 | } 120 | 121 | tips!.add(new: self.pugImage, text: "Best dog ever <3 <3 ^_^ ^_^", with: pugDescriptionConfig.with{$0.position = .right}) 122 | 123 | addAttributedTextTip() 124 | 125 | tips!.add(new: self.pugName, text: "My name is leo ^_^", with: pugDescriptionConfig.with{ 126 | $0.position = .top 127 | if #available(iOS 10.0, *) { 128 | $0.backgroundColor = UIColor(displayP3Red: 0.451, green: 0.807, blue: 0.317, alpha: 1) 129 | } else { 130 | // Fallback on earlier versions 131 | } 132 | }) 133 | 134 | tips!.add( new: self.pugDescrription,text: "I am single and looking for my soulmate", with: pugDescriptionConfig.with{ 135 | $0.position = .bottom 136 | if #available(iOS 10.0, *) { 137 | $0.backgroundColor = UIColor(displayP3Red: 0.451, green: 0.807, blue: 0.317, alpha: 1) 138 | } else { 139 | // Fallback on earlier versions 140 | } 141 | }) 142 | 143 | tips!.add(new: self.transformedButton,text: "Please tap on the \(rotationDegree < 0 ? "☀️" : "🌑").", with: transformed.with{ 144 | $0.position = .left 145 | $0.changeDimColor = UIColor(red:0.02, green:0.18, blue:0.23, alpha:1.0) 146 | $0.onTargetAreaTap = {[weak self] item in 147 | guard var degree = self?.rotationDegree else {return} 148 | self?.startDayAndNight = true 149 | degree = (degree * -1) 150 | self?.rotationDegree = degree 151 | guard let label = item.contentView as? UILabel else { 152 | return 153 | } 154 | label.text = "Please tap on the \(degree < 0 ? "☀️" : "🌑")" 155 | } 156 | $0.onBubbleTap = {[unowned self]_ in 157 | self.startDayAndNight = false 158 | self.tips?.pointer.options = defaultTipOption 159 | } 160 | }) 161 | 162 | tips!.add(new: self.noConstraintsButton,text: "Hi!", with:transformed.with{ $0.backgroundColor = .red }) 163 | 164 | tips!.add( 165 | new: SimpleTipTarget(on: CGRect(x: UIScreen.main.bounds.midX - 50, y: UIScreen.main.bounds.midY - 50, width: 100, height: 100),cornerRadius: 50), 166 | text: "Tip with no target view, just an arbitrary fixed position", with: transformed.with{$0.backgroundColor = .red}) 167 | 168 | tips!.add( 169 | new: self.bigBottomButton, 170 | text: "A long piece of tip text which needs to span over multiple lines. This tip text will only fit if placed above the target area. This placement is provided for us by TipSee.", 171 | with: transformed.with { 172 | $0.onTargetAreaTap = { [weak self]_ in 173 | guard let degree = self?.rotationDegree else {return} 174 | self?.rotationDegree = (degree * -1) 175 | } 176 | $0.backgroundColor = UIColor.black 177 | $0.finishOnTargetAreaTap = true 178 | } 179 | ) 180 | 181 | tips!.onBubbleTap = {[unowned tips] _ in 182 | tips?.next() 183 | } 184 | 185 | tips!.onDimTap = {[unowned self] _ in 186 | self.startDayAndNight = false 187 | guard let tips = self.tips else {return} 188 | tips.pointer.options = defaultTipOption 189 | if let index = tips.currentIndex,tips.tips.count == (index + 1) { 190 | tips.finish() 191 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { 192 | self.tips = nil 193 | }) 194 | } 195 | 196 | tips.next() 197 | } 198 | self.tips!.next() 199 | } 200 | 201 | override func viewDidAppear(_ animated: Bool) { 202 | super.viewDidAppear(animated) 203 | self.navigationController?.navigationBar.barStyle = .black 204 | 205 | self.showTips() 206 | } 207 | 208 | private func addAttributedTextTip() { 209 | 210 | let boldAttributes = [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 15, weight: .bold)] 211 | let normalAttributes = [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 15, weight: .regular)] 212 | let puppiesText = NSMutableAttributedString(string: "Soon to have some ", attributes: normalAttributes) 213 | let boldStringPart = NSMutableAttributedString(string: " puppies", attributes: boldAttributes) 214 | 215 | puppiesText.append(boldStringPart) 216 | 217 | // create attachment 218 | let imageAttachment = NSTextAttachment() 219 | imageAttachment.image = UIImage(named: "pug_puppy") 220 | 221 | // wrap the attachment in its own attributed string so we can append it 222 | let imageString = NSAttributedString(attachment: imageAttachment) 223 | 224 | // add the NSTextAttachment wrapper to our string, then add some more text. 225 | puppiesText.append(imageString) 226 | puppiesText.append(imageString) 227 | puppiesText.append(imageString) 228 | puppiesText.append(NSAttributedString(string: " !!!")) 229 | 230 | tips!.add(new: self.pugImage, text: puppiesText, with: pugDescriptionConfig.with{$0.position = .right}) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Example/TipSee/images/TipSee.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/images/TipSee.gif -------------------------------------------------------------------------------- /Example/TipSee/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Example/TipSee/images/logo.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 farshadjahanmanesh 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TipSee", 8 | platforms: [.iOS(.v9)], 9 | products: [ 10 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 11 | .library( 12 | name: "TipSee", 13 | targets: ["TipSee"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 22 | .target( 23 | name: "TipSee", 24 | dependencies: []), 25 | .testTarget( 26 | name: "TipSeeTests", 27 | dependencies: ["TipSee"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | 6 | # TipSee 7 | ### TipSee is a lightweight and highly customizable library that helps you to show beautiful tips and hints. 8 | 9 | [![CI Status](https://img.shields.io/travis/farshadjahanmanesh/TipSee.svg?style=flat)](https://travis-ci.org/farshadjahanmanesh/TipSee) 10 | [![Version](https://img.shields.io/cocoapods/v/TipC.svg?style=flat)](https://cocoapods.org/pods/TipSee) 11 | [![License](https://img.shields.io/cocoapods/l/TipC.svg?style=flat)](https://cocoapods.org/pods/TipSee) 12 | [![Platform](https://img.shields.io/cocoapods/p/TipC.svg?style=flat)](https://cocoapods.org/pods/TipSee) 13 | 14 | ### To do: 15 | 16 | - [x] Live change 17 | - [x] Touchable target area 18 | - [x] Animating between tips 19 | - [x] Multipe Tip in the screen 20 | - [x] Dim animation 21 | - [x] bubble animation 22 | - [x] multipleTip, one Location 23 | - [ ] it is good for tip to follow it's target area movements 24 | 25 | # What we can do with TipSee? 26 | We can show interactive hints on top of the views or where ever we want but finding the best place to put the bubble (or custom view) is based on the TipSee's decision. it will find best place to show the hint by considering the available space and the content size, smartly. we can show custom views (like that heart) or simple text as you've seen in the Gif. Tips can point to all kind of views like button, images and ... or just a specific part of the view controller(like the hint that points to the center of the view in the gif) 27 | 28 | 29 | # Customization options: 30 | There are two types of configuration; 31 | * Global, base configuration of all tips within a given instance of TipSee - **TipSee.Options** 32 | * Individual tip configuration - **TipSee.Options.Bubble** 33 | 34 | If a both a base and per tip configuration is set, then the per tip configuration will take precendence. 35 | 36 | **Global Options** 37 | ```swift 38 | public struct Options: TipSeeConfiguration { 39 | 40 | /// buble's options, bubbles will get the default if nothings set 41 | public var bubbles: Bubble 42 | 43 | /// default dim's color, each bubble could changes this color(optionaly) by setting the bubble.dimBackgroundColor 44 | public var dimColor: UIColor 45 | 46 | /// bubble's life cycle. 47 | /// forEver : bubbles will be visible and needs to be remove manualy by caliing dismiss(item), you can show multiple bubbles same time 48 | /// untilNext: everytime show() function is called, previous bubble(if exists) will remove and new one will present 49 | public var bubbleLiveDuration: BubbleLiveDuration 50 | 51 | /// indicates the default bubble's position, each bubble can has specific position by setting bubble.position 52 | public var defaultBubblePosition: BubblePosition 53 | 54 | /// specifies the hole's(Target Area) radius 55 | /// keepTargetAreaCornerRadius : uses target view layer corner radius 56 | /// constantRadius(radius) : sets constant radius for all 57 | /// defaultOrGreater(default) : sets a constant default value or uses the target view layer corner radius if it is greater that the default value 58 | /// none : no corner rradius 59 | public var holeRadius: HoleRadius 60 | 61 | /// indicates bubble's margin 62 | public var safeAreaInsets: UIEdgeInsets 63 | 64 | /// if true, dim will fade after one second 65 | public var dimFading: Bool 66 | 67 | /// default is false. It true, touches on the dimmed area will be passed through 68 | public var shouldPassTouchesThroughDimmingArea: Bool 69 | 70 | public var holePositionChangeDuration: TimeInterval 71 | } 72 | ``` 73 | **Bubble Options** 74 | ```swift 75 | public struct Bubble: TipSeeConfiguration { 76 | 77 | /// bubble's background color 78 | public var backgroundColor: UIColor 79 | 80 | /// preferred position for the bubble 81 | public var position: BubblePosition? 82 | 83 | /// text's font 84 | public var font: UIFont 85 | 86 | /// text's color 87 | public var foregroundColor: UIColor 88 | 89 | /// text's alignment 90 | public var textAlignments: NSTextAlignment 91 | 92 | /// bubble's appearance animation (bounce + fade-in) 93 | public var hasAppearAnimation: Bool 94 | 95 | /// distance between the bubble and the target view 96 | public var padding: UIEdgeInsets = .zero 97 | 98 | /// Whole tip (dimming and bubble) should be dismissed when user taps on the target area. 99 | public var finishOnTargetAreaTap: Bool 100 | 101 | /// default is false. It true, touches on target area will be passed through 102 | public var shouldPassTouchesThroughTargetArea: Bool 103 | 104 | /// will execute when user taps on target area 105 | public var onTargetAreaTap: TapGesture? 106 | 107 | /// each tip could has a different dim color 108 | public var changeDimColor: UIColor? 109 | 110 | /// Whole tip (dimming and bubble) should be dismissed when user taps on the bubble. 111 | public var finishOnBubbleTap: Bool 112 | 113 | /// will execute when user taps on the bubble 114 | public var onBubbleTap: TapGesture? 115 | } 116 | ``` 117 | 118 | ## Actions 119 | TipSee has four actions which we can react to them to handle some situations 120 | 1. **Bubble.onBubbleTap** when user clicks on the bubble view, we can access to tapped bubble and item 121 | 2. **Bubble.onTargetAreaTap** when user click on target area 122 | 3. **HintObject.onDimTap** when user clicks on dim(background), we can access to latest hint on the screen 123 | 4. **HintObject.onBubbleTap** when user clicks on bubble, this is default action if the bubble has not specified one for itself 124 | 125 | ## Tip Lifecycle 126 | based on what we set for **bubbleLiveDuration** in options, tips have a life duration which means that tips should be on the screen until user taps on them to dismiss or should be removed before new one is appearing 127 | 128 | ## How To Use 129 | first thing is setuping your option (or using default option) and then creating a new instance of TipSee 130 | ```swift 131 | let defaultTipOption = TipSee.Options 132 | .default() 133 | .with { 134 | $0.dimColor = UIColor.black.withAlphaComponent(0.3) 135 | $0.bubbleLiveDuration = .untilNext 136 | $0.dimFading = false 137 | } 138 | 139 | // TipSee needs a window to show it's content in it 140 | let tipsee = TipSee(on: self.view.window!) 141 | 142 | // shows a simple text(tip) that is pointed to the view(pugImage), this tip has not specific configuration 143 | // so it will use default one 144 | tipSee.show(for: self.pugImage, text: "good boy") 145 | ``` 146 | 147 | or you can create a custom tip with customized configuration 148 | ```swift 149 | // creates new custom item with custom configs 150 | let pugLoveConfig = TipSee.Options.Bubble 151 | .default() 152 | .with{ 153 | $0.backgroundColor = .clear 154 | $0.foregroundColor = .black 155 | $0.textAlignments = .left 156 | $0.padding = UIEdgeInsets.init(top: 0, left: 16, bottom: 0, right: 16) 157 | $0.position = .top 158 | } 159 | tipSee.show(item TipSee.TipItem.init(ID: "100", pointTo: self.pugImage, contentView: image,bubbleOptions: pugLoveConfig)) 160 | 161 | ``` 162 | 163 | in above exmaple wee need to handle tip sequence ourselves. next, previous, presenting or dismissing should be handled by using the actions like bubble tap, target area tap, dimtap and ... but there is a slideshow extension which we talk in neext section that helps us with these things. 164 | 165 | ## TipSeeManager 166 | TipSee manager is a helper class that gives us the ability to have a slideshow like Tips. this manager handles tips array and provides handy apis (next, previous). we can add tips as many as we want and then start the sequence by calling **.next()** 167 | 168 | ```swift 169 | let defaultTipOption = TipSee.Options 170 | .default() 171 | .with { 172 | $0.dimColor = UIColor.black.withAlphaComponent(0.3) 173 | $0.bubbleLiveDuration = .untilNext 174 | $0.dimFading = false 175 | } 176 | 177 | let tipManager = TipcManager(on: self.view.window!,with: defaultTipOption) 178 | tipManager.add(new: TipSee.TipItem.init(ID: "100", pointTo: self.pugImage, contentView: image,bubbleOptions: pugLoveConfig)) 179 | tipManager.add(new: self.pugImage,text:"best dog ever <3 <3 ^_^ ^_^",with: pugDescriptionConfig.with{$0.position = .right}) 180 | tipManager.add... 181 | ... 182 | ... 183 | ... 184 | 185 | tipManager.next() 186 | 187 | ``` 188 | 189 | 190 | ## Example 191 | 192 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 193 | 194 | ## Requirements 195 | this library doe's not need any requirement and has written in Swift5. 196 | ## Installation 197 | 198 | #### Using [CocoaPods](https://cocoapods.org) 199 | 200 | Edit your `Podfile` and specify the dependency: 201 | 202 | ```ruby 203 | pod 'TipSee' 204 | ``` 205 | 206 | #### Using [Swift Package Manager](https://github.com/apple/swift-package-manager) 207 | 208 | Once you have your Swift package set up, adding `TipSee` as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 209 | 210 | ```swift 211 | dependencies: [ 212 | .package(url: "https://github.com/farshadjahanmanesh/TipSee.git", from: "1.6.0") 213 | ] 214 | ``` 215 | 216 | ## Author 217 | farshadjahanmanesh, farshadjahanmanesh@gmail.com 218 | 219 | ## Contributors 220 | [lawmaestro](https://github.com/lawmaestro) 221 | 222 | ## License 223 | 224 | TipSee is available under the MIT license. See the LICENSE file for more info. the demo's design is something that i've took from dribbble and is blong to https://dribbble.com/harshgopal 225 | -------------------------------------------------------------------------------- /Sources/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valentinjahanmanesh/TipSee/5e7c780ddcfe8a6609a72190a545d53cbdd1f561/Sources/Assets/.gitkeep -------------------------------------------------------------------------------- /Sources/TipSee/BubbleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BubbleView.swift 3 | // TipSee 4 | // 5 | // Created by Farshad Jahanmanesh on 8/14/19. 6 | // 7 | 8 | import UIKit 9 | 10 | public class BubbleView: UIView { 11 | public private(set) var insideView: UIView? 12 | private let cornerRadius: CGFloat = 4 13 | private lazy var shape: CAShapeLayer = { 14 | let shape = CAShapeLayer() 15 | shape.rasterizationScale = UIScreen.main.scale 16 | shape.shouldRasterize = true 17 | return shape 18 | }() 19 | 20 | var backColor: UIColor? { 21 | didSet { setNeedsLayout() } 22 | } 23 | 24 | public struct Arrow { 25 | public struct Position { 26 | public enum Distance { 27 | case mid(offset:CGFloat) 28 | case constant(_ offset:CGFloat) 29 | } 30 | public var distance: Distance 31 | public var edge: CGRectEdge 32 | } 33 | public var position: Position 34 | public var size: CGSize 35 | } 36 | 37 | public var arrow: Arrow = .init(position: .init(distance: .constant(0), edge: .minXEdge), size: .zero) { 38 | didSet { setNeedsLayout() } 39 | } 40 | 41 | override init(frame: CGRect) { 42 | super.init(frame: frame) 43 | //default value 44 | backgroundColor = .clear//UIColor(red: 0.18, green: 0.52, blue: 0.92, alpha: 1.0) 45 | 46 | configureView() 47 | } 48 | 49 | required init?(coder aDecoder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | private func configureView() { 54 | layer.insertSublayer(shape, at: 0) 55 | backgroundColor = .clear 56 | layer.shadowOpacity = 0.2 57 | layer.shadowOffset = CGSize(width: 0, height: 1) 58 | layer.shadowRadius = 1.0 59 | layer.shadowColor = UIColor.black.cgColor 60 | } 61 | 62 | func setContent(view: UIView, padding: CGFloat = 0) { 63 | //remove all subviews 64 | self.subviews.forEach { 65 | $0.removeFromSuperview() 66 | } 67 | 68 | self.addSubview(view) 69 | insideView = view 70 | if self.translatesAutoresizingMaskIntoConstraints { 71 | view.frame.origin = CGPoint(x: padding, y: padding) 72 | view.frame.size = self.frame.insetBy(dx: padding, dy: padding).size 73 | view.removeConstraints(view.constraints) 74 | }else { 75 | self.translatesAutoresizingMaskIntoConstraints = true 76 | view.translatesAutoresizingMaskIntoConstraints = false 77 | if #available(iOS 11.0, *) { 78 | view.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: -padding).isActive = true 79 | view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: padding).isActive = true 80 | view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: -padding).isActive = true 81 | view.leftAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leftAnchor, constant: padding).isActive = true 82 | 83 | } else { 84 | view.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -padding).isActive = true 85 | view.topAnchor.constraint(equalTo: self.topAnchor, constant: padding).isActive = true 86 | view.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding).isActive = true 87 | view.leftAnchor.constraint(equalTo: self.leftAnchor, constant: padding).isActive = true 88 | } 89 | } 90 | } 91 | 92 | private func getStandardArrowCenterOffset() -> CGFloat { 93 | let minOffset = arrow.size.width/2 + cornerRadius 94 | let viewWidth = self.frame.width 95 | switch arrow.position.distance { 96 | case .mid(let offset): 97 | switch arrow.position.edge { 98 | case .maxYEdge, .minYEdge: 99 | return max(minOffset, min(offset + self.bounds.midX, self.frame.width - minOffset)) 100 | default : 101 | return max(minOffset, min(offset + self.bounds.midY, self.frame.height - minOffset)) 102 | } 103 | case .constant(let offset): 104 | return max(minOffset, min(offset, viewWidth - minOffset)) 105 | } 106 | } 107 | 108 | public override func layoutSubviews() { 109 | super.layoutSubviews() 110 | 111 | let path = UIBezierPath() 112 | 113 | let height = self.frame.height 114 | let width = self.frame.width 115 | let minX = self.bounds.minX 116 | let minY = self.bounds.minY 117 | let arrowWidth = arrow.size.width 118 | let arrowHeight = arrow.size.height 119 | let offset = getStandardArrowCenterOffset() 120 | 121 | switch arrow.position.edge { 122 | case .minYEdge: 123 | path.move(to: CGPoint(x: minX + offset - arrowWidth/2, y: minY)) 124 | path.addLine(to: CGPoint(x: minX + offset, y: minY - arrowHeight)) 125 | path.addLine(to: CGPoint(x: minX + offset + arrowWidth/2, y: minY)) 126 | case .maxYEdge: 127 | path.move(to: CGPoint(x: minX + offset - arrowWidth/2, y: minY + height)) 128 | path.addLine(to: CGPoint(x: minX + offset, y: minY + height + arrowHeight)) 129 | path.addLine(to: CGPoint(x: minX + offset + arrowWidth/2, y: minY + height)) 130 | case .maxXEdge: 131 | path.move(to: CGPoint(x: minX + width, y: minY + offset - arrowWidth/2)) 132 | path.addLine(to: CGPoint(x: minX + width + arrowHeight, y: minY + offset)) 133 | path.addLine(to: CGPoint(x: minX + width, y: minY + offset + arrowWidth/2)) 134 | default: 135 | path.move(to: CGPoint(x: minX, y: minY + offset - arrowWidth/2)) 136 | path.addLine(to: CGPoint(x: minX - arrowHeight, y: minY + offset)) 137 | path.addLine(to: CGPoint(x: minX, y: minY + offset + arrowWidth/2)) 138 | } 139 | 140 | let roundedRectPath = UIBezierPath( 141 | roundedRect: CGRect(x: minX, y: minY, width: width, height: height), 142 | cornerRadius: cornerRadius 143 | ) 144 | 145 | path.append(roundedRectPath) 146 | shape.fillColor = backColor?.cgColor 147 | shape.path = path.cgPath 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/TipSee/NSAttributedString+Size.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+Size.swift 3 | // TipSee 4 | // 5 | // Created by Adam Law on 01/03/2020. 6 | // 7 | 8 | import UIKit 9 | 10 | extension NSAttributedString { 11 | func height(widthConstraint: CGFloat) -> CGFloat { 12 | let rect = self.boundingRect(with: CGSize(width: widthConstraint, height: CGFloat.greatestFiniteMagnitude), 13 | options: [.usesLineFragmentOrigin, .usesFontLeading], 14 | context: nil) 15 | return ceil(rect.size.height) 16 | } 17 | func width(widthConstraint: CGFloat, heightConstraint: CGFloat) -> CGFloat { 18 | let rect = self.boundingRect(with: CGSize(width: widthConstraint, height: heightConstraint), 19 | options: [.usesLineFragmentOrigin, .usesFontLeading], 20 | context: nil) 21 | return ceil(rect.size.width) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/TipSee/TipItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipItem.swift 3 | // TipSee 4 | // 5 | // Created by Farshad Jahanmanesh on 8/14/19. 6 | // 7 | 8 | import UIKit 9 | 10 | extension TipSee.TipItem { 11 | public static func == (lhs: TipSee.TipItem, rhs: TipSee.TipItem) -> Bool { 12 | return lhs.ID == rhs.ID 13 | } 14 | } 15 | 16 | extension TipSee { 17 | public struct TipItem: Equatable { 18 | public typealias ID = String 19 | public var ID: ID 20 | public var pointTo: AnyTipTraget 21 | public var contentView: UIView 22 | public var bubbleOptions: TipSee.Options.Bubble? 23 | public init(ID: ID = UUID().uuidString, pointTo: TipTarget, contentView: UIView) { 24 | self.ID = ID 25 | self.pointTo = AnyTipTraget.init(tipTarget: pointTo) 26 | self.contentView = contentView 27 | } 28 | 29 | 30 | public init(ID: ID = UUID().uuidString, pointTo: TipTarget, contentView: UIView, bubbleOptions: TipSee.Options.Bubble?) { 31 | self.ID = ID 32 | self.pointTo = AnyTipTraget.init(tipTarget: pointTo) 33 | self.contentView = contentView 34 | self.bubbleOptions = bubbleOptions 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/TipSee/TipOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipOption.swift 3 | // TipSee 4 | // 5 | // Created by Farshad Jahanmanesh on 8/14/19. 6 | // 7 | 8 | import UIKit 9 | 10 | extension TipSee { 11 | 12 | public enum BubbleLiveDuration { 13 | case forever 14 | case untilNext 15 | // case until(second:TimeInterval) 16 | } 17 | 18 | public enum HoleRadius { 19 | /// uses target view layer corner radius 20 | case keepTargetAreaCornerRadius 21 | 22 | /// sets constant radius for all 23 | case constantRadius(radius: CGFloat) 24 | 25 | /// sets a constant default value or uses the target view layer corner radius if it is greater that the default value 26 | case defaultOrGreater(default: CGFloat) 27 | 28 | /// no corner rradius 29 | case none 30 | } 31 | 32 | public struct Options: TipSeeConfiguration { 33 | public typealias BubblePosition = UIRectEdge 34 | public struct Bubble: TipSeeConfiguration { 35 | 36 | /// bubble's background color 37 | public var backgroundColor: UIColor 38 | 39 | /// preferred position for the bubble 40 | public var position: BubblePosition? 41 | 42 | /// text's font 43 | public var font: UIFont 44 | 45 | /// text's color 46 | public var foregroundColor: UIColor 47 | 48 | /// text's alignment 49 | public var textAlignments: NSTextAlignment 50 | 51 | /// bubble's appearance animation (bounce + fade-in) 52 | public var hasAppearAnimation: Bool 53 | 54 | /// distance between the bubble and the target view 55 | public var padding: UIEdgeInsets = .zero 56 | 57 | /// Whole tip (dimming and bubble) should be dismissed when user taps on the target area. 58 | public var finishOnTargetAreaTap: Bool 59 | 60 | /// default is false. It true, touches on target area will be passed through 61 | public var shouldPassTouchesThroughTargetArea: Bool 62 | 63 | /// will execute when user taps on target area 64 | public var onTargetAreaTap: TapGesture? 65 | 66 | /// each tip could has a different dim color 67 | public var changeDimColor: UIColor? 68 | 69 | /// Whole tip (dimming and bubble) should be dismissed when user taps on the bubble. 70 | public var finishOnBubbleTap: Bool 71 | 72 | /// will execute when user taps on the bubble 73 | public var onBubbleTap: TapGesture? 74 | 75 | public static func `default`()->TipSee.Options.Bubble { 76 | return Options.Bubble( 77 | backgroundColor: .red, 78 | position: nil, 79 | font: UIFont.boldSystemFont(ofSize: 15), 80 | foregroundColor: UIColor.white, 81 | textAlignments: .center, 82 | hasAppearAnimation: true, 83 | padding: .init(top: 16, left: 16, bottom: 16, right: 16), 84 | finishOnTargetAreaTap: false, 85 | shouldPassTouchesThroughTargetArea: false, 86 | onTargetAreaTap: nil, 87 | changeDimColor: nil, 88 | finishOnBubbleTap: false, 89 | onBubbleTap: nil) 90 | } 91 | } 92 | 93 | /// buble's options, bubbles will get the default if nothings set 94 | public var bubbles: Bubble 95 | 96 | /// default dim's color, each bubble could changes this color(optionaly) by setting the bubble.dimBackgroundColor 97 | public var dimColor: UIColor 98 | 99 | /// bubble's life cycle. 100 | /// forEver : bubbles will be visible and needs to be remove manualy by caliing dismiss(item), you can show multiple bubbles same time 101 | /// untilNext: everytime show() function is called, previous bubble(if exists) will remove and new one will present 102 | public var bubbleLiveDuration: BubbleLiveDuration 103 | 104 | /// indicates the default bubble's position, each bubble can has specific position by setting bubble.position 105 | public var defaultBubblePosition: BubblePosition 106 | 107 | /// specifies the hole's(Target Area) radius 108 | /// keepTargetAreaCornerRadius : uses target view layer corner radius 109 | /// constantRadius(radius) : sets constant radius for all 110 | /// defaultOrGreater(default) : sets a constant default value or uses the target view layer corner radius if it is greater that the default value 111 | /// none : no corner rradius 112 | public var holeRadius: HoleRadius 113 | 114 | /// indicates bubble's margin 115 | public var safeAreaInsets: UIEdgeInsets 116 | 117 | /// if true, dim will fade after one second 118 | public var dimFading: Bool 119 | 120 | /// default is false. It true, touches on the dimmed area will be passed through 121 | public var shouldPassTouchesThroughDimmingArea: Bool 122 | 123 | public var holePositionChangeDuration: TimeInterval 124 | public static func `default`()->TipSee.Options { 125 | return Options( 126 | bubbles: Options.Bubble.default(), 127 | dimColor: UIColor.black.withAlphaComponent(0.7), 128 | bubbleLiveDuration: .forever, 129 | defaultBubblePosition: .left, 130 | holeRadius: .defaultOrGreater(default: 8), 131 | safeAreaInsets: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16), 132 | dimFading: true, 133 | shouldPassTouchesThroughDimmingArea: false, 134 | holePositionChangeDuration: 0.5) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/TipSee/TipSee+AttributedText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipSee+AttributedText.swift 3 | // TipSee 4 | // 5 | // Created by Adam Law on 02/03/2020. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension TipSee { 12 | 13 | /// shows a bubble which points to the given view 14 | /// 15 | /// - Parameters: 16 | /// - item: the view that we want to point at and a text for bubble 17 | /// - bubbleOption: custom options for bubble 18 | /// - Returns: generated item that can use to access to views or dismiss action 19 | @discardableResult 20 | public func show(for view: TipTarget, text string: NSAttributedString, with bubbleOption: Options.Bubble? = nil) -> TipItem { 21 | let viewToShow = createItem(for: view, text: string, with: bubbleOption) 22 | return self.show(item: viewToShow, with: bubbleOption) 23 | } 24 | 25 | @discardableResult 26 | public func createItem(for target: TipTarget, text: NSAttributedString, with bubbleOption: Options.Bubble? = nil) -> TipItem { 27 | return TipItem(ID: UUID().uuidString, pointTo: target, contentView: TipSee.createLabel(for: text, with: bubbleOption, defaultOptions: .default()) as UIView, bubbleOptions: bubbleOption) 28 | } 29 | 30 | /// creates a labelView for useing inside a bubble 31 | /// 32 | /// - Parameter text: label text 33 | /// - Returns: generated label view 34 | private static func createLabel( 35 | for text: NSAttributedString, 36 | with itemOptions: Options.Bubble?, 37 | defaultOptions options: TipSee.Options 38 | ) -> UILabel 39 | { 40 | let label = UILabel() 41 | label.attributedText = text 42 | label.textAlignment = .center 43 | label.lineBreakMode = .byWordWrapping 44 | label.numberOfLines = 0 45 | label.font = itemOptions?.font ?? options.bubbles.font 46 | label.sizeToFit() 47 | label.textColor = itemOptions?.foregroundColor ?? options.bubbles.foregroundColor 48 | return label 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/TipSee/TipSee.swift: -------------------------------------------------------------------------------- 1 | // TipSee.swift 2 | // TipSee 3 | // 4 | // Created by Farshad Jahanmanesh on 7/2/19. 5 | // Copyright © 2019 Tap30. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | public class TipSee: UIView, TipSeeManagerProtocol { 11 | public typealias TapGesture = ((TipItem) -> Void) 12 | /// properties 13 | public var options: Options = Options.default() { 14 | didSet{ 15 | if options.dimColor != oldValue.dimColor { 16 | observeForDimColorChange() 17 | } 18 | } 19 | } 20 | 21 | fileprivate var shadowLayerPath: CGPath? 22 | fileprivate unowned let _window: UIWindow 23 | fileprivate lazy var views: [TipItem] = { 24 | attachToWindow() 25 | return [TipItem]() 26 | }() 27 | 28 | fileprivate lazy var bubbles = { return [BubbleView]() } () 29 | private var isBubbleAnimationInProgress = false 30 | private var isHoleAnimationInProgress = false 31 | private var isAnimationInProgress: Bool { 32 | return isHoleAnimationInProgress || isBubbleAnimationInProgress 33 | } 34 | 35 | fileprivate var latestTip: TipItem? 36 | 37 | /// in a very odd situation, hit test called twice and we want to prevent multiple calls to our functions 38 | fileprivate var touched: (view:UIView?, timeStamp: Date?) 39 | 40 | 41 | public var onBubbleTap: TapGesture? 42 | public var onDimTap: TapGesture? 43 | public var onFinished: (() -> Void)? 44 | 45 | /// shows a bubble which points to the given view 46 | /// 47 | /// - Parameters: 48 | /// - item: the view that we want to point at and a text for bubble 49 | /// - bubbleOption: custom options for bubble 50 | /// - Returns: generated item that can use to access to views or dismiss action 51 | @discardableResult public func show(for view: TipTarget,text string: String, with bubbleOption: Options.Bubble? = nil) -> TipItem { 52 | let viewToShow = createItem(for: view, text: string, with: bubbleOption) 53 | return self.show(item: viewToShow, with: bubbleOption) 54 | } 55 | 56 | @discardableResult public func createItem(for target: TipTarget, text: String, with bubbleOption: Options.Bubble? = nil) -> TipItem { 57 | return TipItem(ID: UUID().uuidString, pointTo: target, contentView: TipSee.createLabel(for: text, with: bubbleOption, defaultOptions: self.options) as UIView, bubbleOptions: bubbleOption) 58 | } 59 | @discardableResult public static func createItem(for target: TipTarget, text: String, with bubbleOption: Options.Bubble? = nil) -> TipItem { 60 | return TipItem(ID: UUID().uuidString, pointTo: target, contentView: TipSee.createLabel(for: text, with: bubbleOption, defaultOptions: .default()) as UIView, bubbleOptions: bubbleOption) 61 | } 62 | 63 | private final func clearAllViews() { 64 | guard !views.isEmpty else{return} 65 | views.forEach { (item) in 66 | self.dismiss(item: item) 67 | } 68 | } 69 | 70 | private final func store(tip: TipItem, bubble: BubbleView) { 71 | self.latestTip = tip 72 | self.views.append(tip) 73 | self.bubbles.append(bubble) 74 | } 75 | 76 | private final func deStore(index: Int) { 77 | self.latestTip = nil 78 | if self.bubbles.count > index { 79 | self.bubbles.remove(at: index) 80 | } 81 | if self.views.count > index{ 82 | self.views.remove(at: index) 83 | } 84 | } 85 | 86 | /// shows a bubble which points to the given view 87 | /// 88 | /// - Parameters: 89 | /// - item: the view that you want to point at and a view that will show inside the bubble 90 | /// - bubbleOption: custom options for bubble 91 | /// - Returns: generated item that can use to access to views or dismiss action 92 | @discardableResult public func show(item: TipItem, with bubbleOption: Options.Bubble? = nil) -> TipItem { 93 | let tip = TipItem(ID: item.ID.isEmpty ? UUID().uuidString: item.ID, pointTo: item.pointTo, contentView: item.contentView as UIView, bubbleOptions: bubbleOption ?? item.bubbleOptions) 94 | if options.bubbleLiveDuration == .untilNext { 95 | clearAllViews() 96 | } 97 | store(tip: tip, bubble: self.point(to: tip)) 98 | createHoleForVisibleViews() 99 | return tip 100 | } 101 | 102 | private final func attachToWindow() { 103 | self.frame = _window.frame 104 | self.tag = 9891248 105 | _window.addSubview(self) 106 | _window.bringSubviewToFront(self) 107 | } 108 | 109 | /// has animation 110 | public func finish() { 111 | UIView.animateKeyframes(withDuration: 0.2, delay: 0, options: [], animations: { 112 | self.alpha = 0 113 | }) { done in 114 | if done { 115 | self.removeFromSuperview() 116 | } 117 | self.onFinished?() 118 | } 119 | } 120 | 121 | /// removes the given item. it will finish tipPointer after dismissing if this is the last item in tips array, if you plan to show another item on dimissal(one by one) create an array of items and add them to the tipPointer and set the bubbleLiveDuration == .untilNext. 122 | /// 123 | /// - Parameter item: item to remove 124 | public func dismiss(item: TipItem) { 125 | if let index = self.views.lastIndex(where: {$0 == item}) { 126 | let bubble = self.bubbles[index] 127 | bubble.removeFromSuperview() 128 | deStore(index: index) 129 | createHoleForVisibleViews() 130 | } 131 | } 132 | 133 | private final func createHoleForVisibleViews() { 134 | //shadowLayer?.removeFromSuperlayer() 135 | guard superview != nil else { return } 136 | let pathBigRect = UIBezierPath(rect: superview!.frame) 137 | if self.views.isEmpty { 138 | return 139 | } 140 | 141 | /// 142 | var startPoint: CGPoint? 143 | let viewsSet = Set(self.views.map({$0.pointTo})) 144 | viewsSet.forEach { (targetArea) in 145 | // cuts a hole inside the layer 146 | let cutPosition = targetArea.tipFrame.insetBy(dx: -4, dy: -4) 147 | if startPoint == nil { 148 | startPoint = CGPoint(x: cutPosition.midX, y: cutPosition.midY) 149 | } 150 | var cornerRadius: CGFloat = 0 151 | switch options.holeRadius { 152 | case .constantRadius(radius: let radius): 153 | cornerRadius = radius 154 | case .defaultOrGreater(default: let radius): 155 | cornerRadius = max(radius, targetArea.cornersRadius) 156 | case .keepTargetAreaCornerRadius : 157 | cornerRadius = targetArea.cornersRadius 158 | default: 159 | cornerRadius = 0 160 | } 161 | pathBigRect.append(UIBezierPath(roundedRect: cutPosition.insetBy(dx: -4, dy: -4), cornerRadius: cornerRadius)) 162 | } 163 | pathBigRect.usesEvenOddFillRule = true 164 | options.dimColor = latestTip?.bubbleOptions?.changeDimColor ?? options.dimColor 165 | self.cutHole(for: pathBigRect.cgPath, startPoint: startPoint) 166 | } 167 | 168 | public init(on window: UIWindow) { 169 | self._window = window 170 | super.init(frame: .zero) 171 | let tap = UITapGestureRecognizer(target: self, action: #selector(self.tapDim(_:))) 172 | self.addGestureRecognizer(tap) 173 | } 174 | 175 | @objc 176 | private func tapDim(_ sender: UITapGestureRecognizer) { 177 | guard let latestTip = latestTip else { 178 | assertionFailure("here, POINTER have to have a tip, so check if something wrong") 179 | return 180 | } 181 | onDimTap?(latestTip) 182 | } 183 | 184 | public required init?(coder aDecoder: NSCoder) { 185 | fatalError("init(coder:) has not been implemented") 186 | } 187 | 188 | @objc 189 | private func tapBubble(_ sender: UITapGestureRecognizer) { 190 | guard let item = self.views.first(where: { item in item.ID == sender.identifier }) else { 191 | assertionFailure("Here we have to have that bubble(tip) but we can not find it") 192 | return 193 | } 194 | if let customGesture = item.bubbleOptions?.onBubbleTap { 195 | customGesture(item) 196 | } else { 197 | onBubbleTap?(item) 198 | } 199 | let option = item.bubbleOptions ?? self.options.bubbles 200 | if option.finishOnBubbleTap { 201 | self.finish() 202 | } 203 | } 204 | 205 | /// points to the given(target) view by constrainting/Positioning the bubbleView and furthermore adds animation to newborn bubble 206 | /// 207 | /// - Parameter item: tip item 208 | /// - Returns: baked bubble view 209 | private final func point(to item: TipItem) -> BubbleView { 210 | self.latestTip = item 211 | let view = item.pointTo 212 | let label = item.contentView 213 | let bubble = defaultBubble(for: item, defaultOptions: options) 214 | .setProperSizeWith(content: label) 215 | 216 | self.addSubview(bubble) 217 | 218 | let tap = UITapGestureRecognizer(target: self, action: #selector(self.tapBubble(_:))) 219 | tap.identifier = item.ID 220 | bubble.addGestureRecognizer(tap) 221 | 222 | // animates the bubble appearing 223 | if let animation = item.bubbleOptions?.hasAppearAnimation, animation { 224 | bubble.transform = .init(scaleX: 0.5, y: 0.5) 225 | self.animateBubble { 226 | bubble.transform = .identity 227 | bubble.alpha = 1 228 | } 229 | } else { 230 | self.animateBubble { 231 | bubble.alpha = 1 232 | } 233 | } 234 | 235 | // binds constraints 236 | let arrowInstalledPosition = self.setBubbleConstraints(for: bubble, to: item) 237 | bubble.setContent(view: label, padding: 8) 238 | self.layoutIfNeeded() 239 | 240 | // align the arrow 241 | let center = CGPoint(x: view.tipFrame.midX, y: view.tipFrame.midY) 242 | 243 | if [.top, .bottom].contains(arrowInstalledPosition) { 244 | bubble.arrow = .init(position: .init(distance: .constant(center.x - bubble.frame.origin.x), edge: arrowInstalledPosition.toCGRectEdge()), size: .init(width: 10, height: 5)) 245 | } else { 246 | bubble.arrow = .init(position: .init(distance: .mid(offset:0), edge: arrowInstalledPosition.toCGRectEdge()), size: .init(width: 10, height: 5)) 247 | } 248 | 249 | return bubble 250 | } 251 | 252 | private func animateBubble(with: @escaping () -> Void) { 253 | isBubbleAnimationInProgress = true 254 | UIView.animate(withDuration: 0.5, delay: 0.2, usingSpringWithDamping: 0.6, initialSpringVelocity: 1, options: .curveLinear, animations: { 255 | with() 256 | }, completion: { [weak self] _ in 257 | self?.isBubbleAnimationInProgress = false 258 | }) 259 | } 260 | 261 | /// you can choose your prefered position for bubble to be shown, but sometimes there are no enough space there for showing that bubble, this will find a better place for bubbleView 262 | /// 263 | /// - Parameters: 264 | /// - view: target view 265 | /// - Returns: the better edge 266 | private func findBetterSpace(view: TipTarget, preferredPosition: UIRectEdge?, bubblePrefereddSize: CGRect?) -> UIRectEdge { 267 | let reletivePosition = view.tipFrame 268 | 269 | var edges = [(UIRectEdge, Bool)]() 270 | 271 | var left = options.safeAreaInsets.left + options.bubbles.padding.right + 16 + (bubblePrefereddSize?.width ?? 0) 272 | 273 | var right = _window.bounds.width - (options.safeAreaInsets.right + options.bubbles.padding.left + 16) - (bubblePrefereddSize?.width ?? 0) 274 | 275 | var top = options.safeAreaInsets.top + options.bubbles.padding.bottom + 16 + (bubblePrefereddSize?.height ?? 0) 276 | 277 | var bottom = _window.bounds.height - ( options.safeAreaInsets.bottom + options.bubbles.padding.top + 16) + (bubblePrefereddSize?.height ?? 0) 278 | if #available(iOS 11.0, *) { 279 | bottom = _window.bounds.height - (_window.safeAreaInsets.bottom + options.safeAreaInsets.bottom + options.bubbles.padding.top + 16) + (bubblePrefereddSize?.height ?? 0) 280 | 281 | top = options.safeAreaInsets.top + options.bubbles.padding.bottom + 16 + _window.safeAreaInsets.top + (bubblePrefereddSize?.height ?? 0) 282 | 283 | right = _window.bounds.width - (_window.safeAreaInsets.right + options.safeAreaInsets.right + options.bubbles.padding.left + 16) - (bubblePrefereddSize?.width ?? 0) 284 | 285 | left = options.safeAreaInsets.left + options.bubbles.padding.right + 16 + _window.safeAreaInsets.left + (bubblePrefereddSize?.width ?? 0) 286 | } 287 | 288 | edges.append((.left, reletivePosition.minX > left)) 289 | 290 | edges.append((.top, reletivePosition.minY > top)) 291 | 292 | edges.append((.right, reletivePosition.maxX < right)) 293 | 294 | edges.append((.bottom, reletivePosition.maxY < bottom)) 295 | 296 | guard let doIHaveEnoughSpace = edges.first(where: {$0.0 == preferredPosition ?? options.defaultBubblePosition}), doIHaveEnoughSpace.1 else { 297 | return edges.first(where: {$0.0 != preferredPosition ?? options.defaultBubblePosition && $0.1})?.0 ?? options.defaultBubblePosition 298 | } 299 | return preferredPosition ?? options.defaultBubblePosition 300 | } 301 | 302 | /// sets constraints for bubble view and the taget view 303 | /// 304 | /// - Parameters: 305 | /// - view: target view 306 | /// - bubble: bubble view 307 | /// - padding: space between bubble view and target view 308 | /// - Returns: bubble view arrow position 309 | private func setBubbleConstraints(for bubble: BubbleView, to item: TipItem) -> UIRectEdge { 310 | let view = item.pointTo 311 | let preferredPosition = item.bubbleOptions?.position 312 | let padding = item.bubbleOptions?.padding ?? UIEdgeInsets.all(16) 313 | let position = findBetterSpace(view: view, preferredPosition: preferredPosition, bubblePrefereddSize: bubble.frame) 314 | var arrowPoint: UIRectEdge = .right 315 | 316 | let targetFrame = view.tipFrame 317 | let controllerSize = self._window.bounds.size 318 | switch position { 319 | case .left: 320 | arrowPoint = .right 321 | bubble.frame.size = findBubbleProperSize(for: item.contentView, on: CGSize(width: abs(targetFrame.minX - (options.safeAreaInsets.totalX + padding.left)), height: controllerSize.height - options.safeAreaInsets.totalY)) 322 | 323 | bubble.frame.origin = CGPoint(x: targetFrame.minX - (padding.right + bubble.frame.size.width), y: targetFrame.midY - bubble.frame.midY) 324 | case .right: 325 | arrowPoint = .left 326 | bubble.frame.size = findBubbleProperSize(for: item.contentView, on: CGSize(width: (controllerSize.width - (options.safeAreaInsets.totalX + padding.left + targetFrame.maxX)), height: controllerSize.height)) 327 | bubble.frame.origin = CGPoint(x: targetFrame.minX + targetFrame.size.width + padding.left, y: targetFrame.midY - bubble.frame.midY) 328 | case .bottom: 329 | arrowPoint = .top 330 | bubble.frame.origin.y = targetFrame.maxY + padding.top 331 | bubble.center.x = view.tipFrame.midX 332 | 333 | case .top: 334 | arrowPoint = .bottom 335 | bubble.frame.origin.y = targetFrame.minY - bubble.frame.size.height - padding.bottom 336 | bubble.center.x = targetFrame.midX 337 | default: 338 | break 339 | } 340 | if bubble.frame.minX < options.safeAreaInsets.left { 341 | bubble.frame.origin.x = options.safeAreaInsets.left 342 | } 343 | if bubble.frame.maxX > controllerSize.width - options.safeAreaInsets.right { 344 | bubble.frame.origin.x -= ((bubble.frame.maxX - controllerSize.width) + options.safeAreaInsets.right) 345 | } 346 | return arrowPoint 347 | } 348 | 349 | private func observeForDimColorChange(){ 350 | guard let shadowPath = self.layer.sublayers?[0] as? CAShapeLayer,shadowPath.fillColor != options.dimColor.cgColor else { 351 | return 352 | } 353 | let pathAnimation = basicAnimation(key: "fillColor", duration: 0.2) 354 | pathAnimation.fromValue = shadowPath.fillColor 355 | pathAnimation.toValue = options.dimColor.cgColor 356 | shadowPath.add(pathAnimation, forKey: nil) 357 | shadowPath.fillColor = options.dimColor.cgColor 358 | } 359 | 360 | private final func cutHole(for path: CGPath, startPoint: CGPoint? = nil) { 361 | 362 | let fillLayer = CAShapeLayer() 363 | 364 | fillLayer.fillRule = CAShapeLayerFillRule.evenOdd 365 | fillLayer.fillColor = options.dimColor.cgColor 366 | 367 | guard let shadowPath = self.layer.sublayers?[0] as? CAShapeLayer else { 368 | self.layer.insertSublayer(fillLayer, at: 0) 369 | let height = 2 * max(_window.bounds.width, _window.bounds.height) 370 | var circleRect = CGRect(origin: startPoint ?? .zero, size: .init(width: height, height: height)) 371 | circleRect.origin.x -= height / 2 372 | circleRect.origin.y -= height / 2 373 | let base = UIBezierPath(rect: _window.bounds) 374 | 375 | base.append( UIBezierPath(roundedRect: circleRect, cornerRadius: height / 2)) 376 | base.usesEvenOddFillRule = true 377 | addHoleAnimations(on: fillLayer, old: base.cgPath, new: path,force: true) 378 | fillLayer.path = path 379 | shadowLayerPath = path 380 | return 381 | } 382 | 383 | shadowPath.path = path 384 | addHoleAnimations(on: shadowPath, old: shadowLayerPath!, new: path) 385 | shadowLayerPath = path 386 | } 387 | 388 | private final func addHoleAnimations(on layer: CAShapeLayer, old: CGPath, new: CGPath, force: Bool = false) { 389 | let pathAnimation = basicAnimation( 390 | key: "path", 391 | duration: self.options.holePositionChangeDuration, 392 | completion: { [weak self] in 393 | self?.isHoleAnimationInProgress = false 394 | } 395 | ) 396 | pathAnimation.fromValue = options.bubbleLiveDuration == .untilNext ? old : new 397 | pathAnimation.toValue = new 398 | layer.add(pathAnimation, forKey: nil) 399 | isHoleAnimationInProgress = true 400 | 401 | if options.dimFading { 402 | let fillColorAnimation = basicAnimation(key: "fillColor", duration: 1) 403 | fillColorAnimation.toValue = UIColor.clear.cgColor 404 | fillColorAnimation.beginTime = CACurrentMediaTime() + 2 405 | layer.add(fillColorAnimation, forKey: nil) 406 | } 407 | } 408 | private final func basicAnimation(key: String, duration: TimeInterval, completion: (() -> Void)? = nil ) -> CABasicAnimation { 409 | 410 | let animation = CABasicAnimation(keyPath: key) 411 | animation.duration = duration 412 | animation.isRemovedOnCompletion = false 413 | animation.fillMode = CAMediaTimingFillMode.forwards 414 | 415 | CATransaction.setCompletionBlock { 416 | completion?() 417 | } 418 | return animation 419 | } 420 | } 421 | 422 | fileprivate extension UIRectEdge { 423 | func toCGRectEdge() -> CGRectEdge { 424 | switch self { 425 | case .top: 426 | return .minYEdge 427 | case .bottom : 428 | return .maxYEdge 429 | case .right : 430 | return .maxXEdge 431 | default: 432 | return .minXEdge 433 | } 434 | } 435 | } 436 | 437 | fileprivate extension UIEdgeInsets { 438 | static func all(_ value: CGFloat) -> UIEdgeInsets { 439 | return UIEdgeInsets(top: value, left: value, bottom: value, right: value) 440 | } 441 | 442 | var totalX: CGFloat { 443 | return self.left + self.right 444 | } 445 | 446 | var totalY: CGFloat { 447 | return self.top + self.bottom 448 | } 449 | } 450 | 451 | extension String { 452 | func height(font: UIFont, widthConstraint: CGFloat) -> CGFloat { 453 | let label = UILabel(frame: CGRect(x: 0, y: 0, width: widthConstraint, height: CGFloat.greatestFiniteMagnitude)) 454 | label.numberOfLines = 0 455 | label.lineBreakMode = NSLineBreakMode.byWordWrapping 456 | label.font = font 457 | label.text = self + " " 458 | 459 | label.sizeToFit() 460 | return label.frame.height 461 | } 462 | func width(font: UIFont, widthConstraint: CGFloat, heightConstraint: CGFloat) -> CGFloat { 463 | let label = UILabel(frame: CGRect(x: 0, y: 0, width: widthConstraint, height: heightConstraint)) 464 | label.numberOfLines = 0 465 | label.lineBreakMode = NSLineBreakMode.byWordWrapping 466 | label.font = font 467 | label.text = self + " " 468 | 469 | label.sizeToFit() 470 | return label.frame.width 471 | } 472 | } 473 | 474 | /// Overrides HitTest and Point inside 475 | extension TipSee { 476 | public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 477 | guard self.touched.timeStamp == nil else { 478 | let touchedView = self.touched.view 479 | self.touched = (nil,nil) 480 | return touchedView 481 | } 482 | let hitView = super.hitTest(point, with: event) 483 | self.touched = (hitView, Date()) 484 | 485 | return hitView 486 | } 487 | 488 | public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 489 | guard shadowLayerPath != nil, let latestTip = latestTip else { 490 | return false 491 | } 492 | if isAnimationInProgress { 493 | // Consume the touch and stop it going any further. 494 | // This ensures tips can't be cycled through / dismissed mid animation which 495 | // can otherwise lead to tips overlaying a mismatched target screen / layout. 496 | return true 497 | } 498 | let targetArea = latestTip.pointTo 499 | let insetTargetArea = targetArea.tipFrame.insetBy(dx: -4, dy: -4) 500 | 501 | let isInTargetArea = insetTargetArea.contains(point) 502 | let option = latestTip.bubbleOptions ?? self.options.bubbles 503 | 504 | if isInTargetArea { 505 | option.onTargetAreaTap?(latestTip) 506 | if option.finishOnTargetAreaTap { 507 | self.finish() 508 | } 509 | 510 | if option.shouldPassTouchesThroughTargetArea == false { 511 | // Consume the touch 512 | return true 513 | } 514 | return false 515 | } 516 | 517 | let isInBubbleArea = bubbles.last?.frame.contains(point) ?? false 518 | if isInBubbleArea { 519 | // Consume the touch 520 | return true 521 | } 522 | 523 | let isInDimmingArea = frame.contains(point) 524 | if isInDimmingArea { 525 | if options.shouldPassTouchesThroughDimmingArea == false { 526 | // Consume the touch 527 | return true 528 | } 529 | return false 530 | } 531 | return false 532 | } 533 | } 534 | 535 | private extension UITapGestureRecognizer { 536 | 537 | private struct AssociatedKeys { 538 | static var identifier = "identifier" 539 | } 540 | 541 | var identifier: String { 542 | get { 543 | return (objc_getAssociatedObject(self, &AssociatedKeys.identifier) as? String) ?? "" 544 | } set { 545 | objc_setAssociatedObject( 546 | self, 547 | &AssociatedKeys.identifier, 548 | newValue, 549 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 550 | ) 551 | } 552 | } 553 | } 554 | 555 | extension TipSee { 556 | 557 | /// creates a default bubble 558 | /// 559 | /// - Parameter item: tip item 560 | /// - Returns: bubble view 561 | final func defaultBubble(for item: TipSee.TipItem, defaultOptions options: TipSee.Options) -> BubbleView { 562 | 563 | let bubble = BubbleView.default() 564 | bubble.backColor = item.bubbleOptions?.backgroundColor ?? options.bubbles.backgroundColor 565 | return bubble 566 | } 567 | 568 | /// creates a labelView for useing inside a bubble 569 | /// 570 | /// - Parameter text: label text 571 | /// - Returns: generated label view 572 | private static func createLabel(for text: String, with itemOptions: Options.Bubble?, defaultOptions options: TipSee.Options) -> UILabel { 573 | let label = UILabel() 574 | label.text = text 575 | label.textAlignment = .center 576 | label.lineBreakMode = .byWordWrapping 577 | label.numberOfLines = 0 578 | label.sizeToFit() 579 | label.font = itemOptions?.font ?? options.bubbles.font 580 | label.textColor = itemOptions?.foregroundColor ?? options.bubbles.foregroundColor 581 | return label 582 | } 583 | 584 | 585 | public func options(_ options: TipSee.Options) { 586 | self.options = options 587 | } 588 | /// finds bubble size 589 | /// 590 | /// - Parameters: 591 | /// - availableSpace: avialable space for bubble to fit in 592 | /// - view: view that live in bubble view 593 | /// - Returns: proper size 594 | private func findBubbleProperSize(for view: UIView,on availableSpace: CGSize? = nil) -> CGSize { 595 | var calculatedFrame = CGSize.zero 596 | let availableSpace = availableSpace ?? CGSize(width: UIScreen.main.bounds.width - (64), height: UIScreen.main.bounds.height) 597 | if let label = view as? UILabel, let text = label.attributedText { 598 | calculatedFrame.height = text.height(widthConstraint: availableSpace.width) + 16 599 | calculatedFrame.width = text.width(widthConstraint: availableSpace.width, heightConstraint: self.frame.size.height) + 16 600 | }else { 601 | calculatedFrame = view.frame.insetBy(dx: -8, dy: -8).size 602 | } 603 | return calculatedFrame 604 | } 605 | } 606 | 607 | extension BubbleView { 608 | /// finds bubble size 609 | /// 610 | /// - Parameters: 611 | /// - availableSpace: avialable space for bubble to fit in 612 | /// - view: view that live in bubble view 613 | /// - Returns: updated bubble view 614 | fileprivate func setProperSizeWith(content view: UIView,on availableSpace: CGSize? = nil) -> BubbleView { 615 | let bubbleView = self 616 | var calculatedFrame = CGSize.zero 617 | let availableSpace = availableSpace ?? CGSize(width: UIScreen.main.bounds.width - (64), height: UIScreen.main.bounds.height) 618 | if let label = view as? UILabel, let text = label.attributedText { 619 | calculatedFrame.height = text.height(widthConstraint: availableSpace.width) + 16 620 | calculatedFrame.width = text.width(widthConstraint: availableSpace.width, heightConstraint: self.frame.size.height) + 16 621 | }else { 622 | calculatedFrame = view.frame.insetBy(dx: -8, dy: -8).size 623 | bubbleView.translatesAutoresizingMaskIntoConstraints = false 624 | } 625 | bubbleView.frame.size = calculatedFrame 626 | return bubbleView 627 | } 628 | 629 | /// creates a default bubble 630 | /// 631 | /// - Parameter item: tip item 632 | /// - Returns: bubble view 633 | fileprivate static func `default`() -> BubbleView { 634 | let bubble = BubbleView(frame: .zero) 635 | bubble.alpha = 0 636 | bubble.arrow = .init(position: .init(distance: .mid(offset: 0), edge: UIRectEdge.left.toCGRectEdge()), size: .init(width: 10, height: 5)) 637 | return bubble 638 | } 639 | } 640 | -------------------------------------------------------------------------------- /Sources/TipSee/TipSeeConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipSeeConfiguration.swift 3 | // TipSee 4 | // 5 | // Created by Farshad on 10/30/19. 6 | // 7 | 8 | public protocol TipSeeConfiguration { 9 | func with(_ mutations: (inout Self) -> Void) -> Self 10 | } 11 | 12 | extension TipSeeConfiguration { 13 | public func with(_ mutations: (inout Self) -> Void) -> Self { 14 | var copyOfSelf = self 15 | mutations(©OfSelf) 16 | return copyOfSelf 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/TipSee/TipSeeManager+AttributedText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipSeeManager+AttributedText.swift 3 | // TipSee 4 | // 5 | // Created by Adam Law on 03/01/2020. 6 | // 7 | 8 | import UIKit 9 | 10 | extension TipSeeManager { 11 | 12 | /// Adds a tip which targets a given object conforming to `TipTarget` 13 | /// - Parameters: 14 | /// - target: Target object used for pinning the tip to. 15 | /// - string: Attributed tip text to show in the bubble. 16 | /// - bubbleOptions: Bubble appearance configuration options. 17 | public func add( 18 | new target: TipTarget, 19 | text string: NSAttributedString, 20 | with bubbleOptions: TipSee.Options.Bubble?) 21 | { 22 | self.tips.append(pointer.createItem(for: target, text: string, with: bubbleOptions)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/TipSee/TipSeeManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipSeeManager.swift 3 | // TipSee 4 | // 5 | // Created by Farshad Jahanmanesh on 7/18/19. 6 | // 7 | 8 | import UIKit 9 | 10 | public final class TipSeeManager { 11 | public private(set) var pointer: TipSee 12 | public internal(set) var tips: [TipSee.TipItem] 13 | public private(set) var latestTip: TipSee.TipItem? 14 | public var onBubbleTap: ((TipSee.TipItem?) -> Void)? { 15 | didSet { 16 | pointer.onBubbleTap = self.onBubbleTap 17 | } 18 | } 19 | public var onDimTap: ((TipSee.TipItem?) -> Void)? { 20 | didSet { 21 | pointer.onDimTap = self.onDimTap 22 | } 23 | } 24 | public var onFinished: (() -> Void)? { 25 | didSet { 26 | pointer.onFinished = self.onFinished 27 | } 28 | } 29 | public private(set) var currentIndex: Int? { 30 | didSet { 31 | guard let index = currentIndex else { 32 | self.pointer.finish() 33 | return 34 | } 35 | latestTip = tips[index] 36 | self.pointer.show(item: latestTip!) 37 | } 38 | } 39 | 40 | /// Designated intializer. 41 | /// - Parameters: 42 | /// - window: The window used for attaching the tip view to. 43 | /// - options: Manager appearance configuration options. 44 | public init(on window: UIWindow, with options: TipSee.Options) { 45 | self.pointer = TipSee(on: window) 46 | self.pointer.options(options) 47 | self.tips = [TipSee.TipItem]() 48 | } 49 | 50 | /// Adds a tip which targets a given object conforming to `TipTarget` 51 | /// - Parameters: 52 | /// - view: Target object used for pinning the tip to. 53 | /// - string: Tip text to show in the bubble. 54 | /// - bubbleOptions: Bubble appearance configuration options. 55 | public func add(new view: TipTarget, text string: String, with bubbleOption: TipSee.Options.Bubble?) { 56 | self.tips.append(pointer.createItem(for: view,text: string, with: bubbleOption)) 57 | } 58 | 59 | /// Adds a tip which targets a given object conforming to `TipTarget` 60 | /// - Parameters: 61 | /// - item: Target object used for pinning the tip to. 62 | public func add(new item: TipSee.TipItem) { 63 | self.tips.append(item) 64 | } 65 | 66 | /// shows the next tip 67 | public func next() { 68 | guard let current = latestTip, let currentIndex = tips.firstIndex(of: current) else { 69 | if !tips.isEmpty{ 70 | self.currentIndex = 0 71 | } 72 | return 73 | } 74 | let next = currentIndex+1 75 | if next < tips.count { 76 | self.currentIndex = next 77 | } else { 78 | finish() 79 | } 80 | } 81 | 82 | // shows the previous tip 83 | public func previous() { 84 | guard let current = latestTip, let currentIndex = tips.firstIndex(of: current) else { 85 | return 86 | } 87 | let previous = currentIndex-1 88 | if previous >= 0 { 89 | self.currentIndex = previous 90 | } 91 | } 92 | 93 | public func finish() { 94 | pointer.finish() 95 | } 96 | } 97 | 98 | extension TipSeeManager { 99 | 100 | public func add( 101 | new view: TipTarget, 102 | texts strings: [String], 103 | with bubbleOption: TipSee.Options.Bubble?, 104 | buttonsConfigs: ((_ previousButton: UIButton, _ nextButton: UIButton) -> Void)) 105 | { 106 | let buttonsHeight : CGFloat = 30 107 | let font = bubbleOption?.font ?? pointer.options.bubbles.font 108 | var containerHeight : CGFloat = 40 109 | var containerWidth : CGFloat = strings.max()?.width(font: font, widthConstraint: UIScreen.main.bounds.width - 48, heightConstraint: 40) ?? 100 110 | 111 | if containerWidth > UIScreen.main.bounds.width { 112 | containerWidth = UIScreen.main.bounds.width - 48 113 | } 114 | containerHeight = strings.max()?.height(font: font, widthConstraint: containerWidth) ?? 100 115 | 116 | let container = UIView(frame: CGRect(x: 0, y: 0, width: containerWidth, height: containerHeight + 40 )) 117 | 118 | // creates a scroll view and appends it to bubble view 119 | let customScrollView = UIScrollView(frame: .zero) 120 | customScrollView.translatesAutoresizingMaskIntoConstraints = false 121 | customScrollView.showsHorizontalScrollIndicator = false 122 | customScrollView.showsVerticalScrollIndicator = false 123 | container.addSubview(customScrollView) 124 | customScrollView.isScrollEnabled = false 125 | customScrollView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: 0.0).isActive = true 126 | customScrollView.topAnchor.constraint(equalTo: container.topAnchor, constant: 0.0).isActive = true 127 | customScrollView.rightAnchor.constraint(equalTo: container.rightAnchor, constant: 0.0).isActive = true 128 | customScrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: buttonsHeight).isActive = true 129 | customScrollView.heightAnchor.constraint(equalToConstant: container.bounds.height - buttonsHeight).isActive = true 130 | customScrollView.widthAnchor.constraint(equalToConstant: container.bounds.width).isActive = true 131 | 132 | // creates a stack view and apeends it to scroll view 133 | let stackView = UIStackView(frame: CGRect(x: 0, y: 0, width: container.frame.width, height: 200)) 134 | customScrollView.addSubview(stackView) 135 | customScrollView.isPagingEnabled = true 136 | stackView.translatesAutoresizingMaskIntoConstraints = false 137 | stackView.leadingAnchor.constraint(equalTo: customScrollView.leadingAnchor).isActive = true 138 | stackView.trailingAnchor.constraint(equalTo: customScrollView.trailingAnchor).isActive = true 139 | stackView.topAnchor.constraint(equalTo: customScrollView.topAnchor).isActive = true 140 | stackView.bottomAnchor.constraint(equalTo: customScrollView.bottomAnchor).isActive = true 141 | stackView.heightAnchor.constraint(equalToConstant: container.bounds.height - buttonsHeight).isActive = true 142 | stackView.alignment = .fill 143 | stackView.distribution = .fillEqually 144 | stackView.axis = .horizontal 145 | 146 | // appends all texts as label inside stack view 147 | strings.forEach { (str) in 148 | let label = UILabel() 149 | label.text = str 150 | label.font = font 151 | label.lineBreakMode = .byWordWrapping 152 | label.numberOfLines = 0 153 | label.textColor = bubbleOption?.foregroundColor ?? pointer.options.bubbles.foregroundColor 154 | stackView.addArrangedSubview(label) 155 | label.widthAnchor.constraint(equalToConstant: container.frame.width).isActive = true 156 | } 157 | 158 | // creates next and previous button 159 | let leftButton = UIButton(frame: CGRect.init(origin: CGPoint(x: 0, y: container.frame.height - 25), size: CGSize(width: 25, height: 25))) 160 | leftButton.setTitleColor(.black, for: .normal) 161 | leftButton.accessibilityHint = "previous" 162 | leftButton.tag = 0 163 | leftButton.addTarget(self, action: #selector(scrollMultiLine(_:)), for: .touchUpInside) 164 | container.addSubview(leftButton) 165 | 166 | let rightButton = UIButton(frame: CGRect.init(origin: .zero, size: CGSize(width: 25, height: 25))) 167 | rightButton.setTitleColor(.black, for: .normal) 168 | rightButton.accessibilityHint = "next" 169 | rightButton.tag = 0 170 | rightButton.addTarget(self, action: #selector(scrollMultiLine(_:)), for: .touchUpInside) 171 | rightButton.frame.origin = CGPoint(x: container.frame.width - rightButton.frame.width, y: container.frame.height - 25) 172 | container.addSubview(rightButton) 173 | 174 | // creates a new item with new created conent view 175 | self.tips.append(.init(pointTo: view, contentView: container, bubbleOptions: bubbleOption)) 176 | 177 | // return those two button so users can change the configurstions 178 | buttonsConfigs(leftButton, rightButton) 179 | } 180 | 181 | @objc 182 | private func scrollMultiLine(_ sender: UIButton) { 183 | guard let action = sender.accessibilityHint, let scrollView = sender.superview?.subviews.first(where: {$0 is UIScrollView }) as? UIScrollView else { 184 | return 185 | } 186 | var frame: CGRect = scrollView.frame 187 | 188 | var page = scrollView.tag 189 | page += action == "next" ? 1 : -1 190 | if page < 0 { 191 | return 192 | } else if (frame.size.width * CGFloat(page)) >= scrollView.contentSize.width { 193 | return 194 | } 195 | scrollView.tag = page 196 | print(page) 197 | frame.origin.x = frame.size.width * CGFloat(page) 198 | frame.origin.y = 0 199 | scrollView.scrollRectToVisible(frame, animated: true) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Sources/TipSee/TipSeeManagerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipSeeManagerProtocol.swift 3 | // TipSee 4 | // 5 | // Created by Farshad on 10/30/19. 6 | // 7 | 8 | public protocol TipSeeManagerProtocol { 9 | /// removes the given item 10 | /// 11 | /// - Parameter item: item to remove 12 | func dismiss(item: TipSee.TipItem) 13 | 14 | @discardableResult 15 | func show(for target: TipTarget, text string: String, with bubbleOption: TipSee.Options.Bubble?) -> TipSee.TipItem 16 | 17 | @discardableResult 18 | func createItem(for target: TipTarget, text string: String, with bubbleOption: TipSee.Options.Bubble?) -> TipSee.TipItem 19 | 20 | /// shows a bubble which points to the given view 21 | /// 22 | /// - Parameters: 23 | /// - item: the view that you want to point at and a view that will show inside the bubble 24 | /// - bubbleOption: custom options for bubble 25 | /// - Returns: generated item that can use to access to views or dismiss action 26 | @discardableResult 27 | func show(item: TipSee.TipItem, with bubbleOption: TipSee.Options.Bubble?) -> TipSee.TipItem 28 | func finish() 29 | } 30 | -------------------------------------------------------------------------------- /Sources/TipSee/TipTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipTarget.swift 3 | // TipSee 4 | // 5 | // Created by Farshad Jahanmanesh on 8/14/19. 6 | // 7 | 8 | import UIKit 9 | 10 | /// TipSee needs would interact with this type only 11 | public protocol TipTarget { 12 | var tipFrame : CGRect {get} 13 | var cornersRadius : CGFloat {get} 14 | } 15 | extension TipTarget where Self : Hashable { 16 | public static func ==(_ lhs : Self, rhs : Self)->Bool { 17 | return lhs.tipFrame == rhs.tipFrame 18 | } 19 | } 20 | 21 | extension UIView : TipTarget { 22 | public var cornersRadius: CGFloat { 23 | get { 24 | return self.layer.cornerRadius 25 | } 26 | } 27 | 28 | public var tipFrame: CGRect { 29 | guard let superView = self.superview else { 30 | return self.frame 31 | } 32 | let point = superView.convert(self.frame.origin, to: nil) 33 | return CGRect(origin: point, size: self.frame.size) 34 | } 35 | } 36 | 37 | 38 | /// TipTarget Type Erasure 39 | public struct AnyTipTraget : TipTarget , Hashable { 40 | public var tipFrame : CGRect {return _tipTarget.tipFrame} 41 | public var cornersRadius : CGFloat {return _tipTarget.cornersRadius} 42 | private var _tipTarget : TipTarget 43 | 44 | init(tipTarget : TipTarget){ 45 | self._tipTarget = SimpleTipTarget(on: tipTarget.tipFrame, cornerRadius: tipTarget.cornersRadius) 46 | } 47 | 48 | public func hash(into hasher: inout Hasher) { 49 | hasher.combine(self.tipFrame.origin.x) 50 | hasher.combine(self.tipFrame.origin.y) 51 | hasher.combine(self.tipFrame.width) 52 | hasher.combine(self.tipFrame.height) 53 | hasher.combine(self.cornersRadius) 54 | } 55 | } 56 | 57 | public struct SimpleTipTarget : TipTarget { 58 | public var tipFrame: CGRect 59 | public var cornersRadius: CGFloat 60 | public init(on target : CGRect,cornerRadius : CGFloat){ 61 | self.tipFrame = target 62 | self.cornersRadius = cornerRadius 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/TipSee/UIViewController+Exts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Exts.swift 3 | // TipSee 4 | // 5 | // Created by Farshad Jahanmanesh on 8/14/19. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIViewController { 11 | var tipManager : TipSee? { 12 | return self.view.window?.viewWithTag(9891248) as? TipSee 13 | } 14 | 15 | fileprivate static func swizzleMethods(original: Selector, swizzled: Selector) { 16 | guard 17 | let originalMethod = class_getInstanceMethod(self, original), 18 | let swizzledMethod = class_getInstanceMethod(self, swizzled) else { return } 19 | method_exchangeImplementations(originalMethod, swizzledMethod) 20 | } 21 | 22 | @objc private func swizzled_keyboardListener_viewDidDisappear(_ animated: Bool) { 23 | swizzled_keyboardListener_viewDidDisappear(animated) 24 | self.tipManager?.finish() 25 | } 26 | 27 | fileprivate static let TipSeeViewDidAppearSwizzler: Void = { 28 | swizzleMethods(original: #selector(viewDidDisappear(_:)), 29 | swizzled: #selector(swizzled_keyboardListener_viewDidDisappear)) 30 | }() 31 | } 32 | -------------------------------------------------------------------------------- /Tests/TipSeeTests/OptionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsTests.swift 3 | // TipSee_Example 4 | // 5 | // Created by Farshad Jahanmanesh on 7/19/19. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import TipSee 11 | 12 | class OptionsTests: XCTestCase { 13 | var sut : TipSee! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | let uiwindow = UIWindow() 18 | sut = TipSee(on: uiwindow) 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | override func tearDown() { 23 | sut = nil 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | super.tearDown() 26 | } 27 | 28 | func testSettingOptions(){ 29 | // given 30 | sut.options = TipSee.Options.default() 31 | 32 | // when 33 | sut.options.dimColor = .red 34 | 35 | // then 36 | XCTAssertTrue(sut.options.dimColor == .red) 37 | } 38 | 39 | func testChangeBubbleOptions(){ 40 | // given 41 | sut.options.bubbles = .default() 42 | 43 | // when 44 | sut.options.bubbles.backgroundColor = .yellow 45 | 46 | XCTAssertTrue(sut.options.bubbles.backgroundColor == .yellow) 47 | } 48 | 49 | func testHintPointerShouldHaveDefaultOptions(){ 50 | XCTAssertNotNil(sut.options.bubbles, "Default bubble Option Should not be nil") 51 | XCTAssertNotNil(sut.options, "Default Option Should not be nil") 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Tests/TipSeeTests/TipItemTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipItemTests.swift 3 | // TipSee_Example 4 | // 5 | // Created by Farshad Jahanmanesh on 7/19/19. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import TipSee 11 | 12 | class TipItemTests: XCTestCase { 13 | let id = "55" 14 | var sut : TipSee.TipItem! 15 | var targetView : UIView! 16 | var bubbleContetView : UIView! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | targetView = UIView() 21 | bubbleContetView = UIView() 22 | sut = TipSee.TipItem(ID: id, pointTo: targetView, contentView: bubbleContetView) 23 | // Put setup code here. This method is called before the invocation of each test method in the class. 24 | } 25 | 26 | override func tearDown() { 27 | sut = nil 28 | targetView = nil 29 | bubbleContetView = nil 30 | super.tearDown() 31 | // Put teardown code here. This method is called after the invocation of each test method in the class. 32 | } 33 | 34 | func testDefaultOption(){ 35 | XCTAssertNil(sut.bubbleOptions) 36 | } 37 | 38 | func testCustomConfig(){ 39 | // when 40 | sut.bubbleOptions = TipSee.Options.Bubble.default().with{ 41 | $0.backgroundColor = .blue 42 | } 43 | 44 | XCTAssertNotNil(sut.bubbleOptions) 45 | 46 | XCTAssertNotNil(sut.bubbleOptions!.backgroundColor == .blue) 47 | } 48 | 49 | func testEquality(){ 50 | // given 51 | var new = TipSee.TipItem(ID: "2", pointTo: UIView(), contentView: UIView()) 52 | 53 | // when 54 | new.ID = id 55 | 56 | // then 57 | XCTAssertEqual(new, sut) 58 | } 59 | 60 | func testCustomConfigInInit(){ 61 | // given 62 | let new = TipSee.TipItem(ID: "2", pointTo: UIView(), contentView: UIView(),bubbleOptions: TipSee.Options.Bubble.default().with{$0.backgroundColor = .blue}) 63 | 64 | // when 65 | sut.bubbleOptions = TipSee.Options.Bubble.default().with{ 66 | $0.backgroundColor = .blue 67 | } 68 | 69 | // then 70 | XCTAssertEqual(new.bubbleOptions!.backgroundColor, sut.bubbleOptions!.backgroundColor) 71 | } 72 | func testCustomBubbeTap(){ 73 | // when 74 | sut.bubbleOptions = TipSee.Options.Bubble.default().with{ 75 | $0.backgroundColor = .blue 76 | $0.onBubbleTap = {_ in } 77 | } 78 | 79 | // then 80 | XCTAssertNotNil(sut.bubbleOptions!.onBubbleTap) 81 | } 82 | 83 | func testCustomFont(){ 84 | // given 85 | let new = TipSee.createItem(for: SimpleTipTarget(on: .zero,cornerRadius: 0), text: "XYS",with: TipSee.Options.Bubble.default().with{$0.font = .italicSystemFont(ofSize: 100)}) 86 | 87 | // then 88 | XCTAssertEqual((new.contentView as! UILabel).font, UIFont.italicSystemFont(ofSize: 100)) 89 | } 90 | 91 | func testMEMORYLEAK(){ 92 | 93 | // given 94 | var xView : UIView? = UIView() 95 | let count = CFGetRetainCount(xView!) 96 | let _ = TipSee.TipItem(ID: "2", pointTo: xView!, contentView: UIView(),bubbleOptions: TipSee.Options.Bubble.default().with{$0.backgroundColor = .blue}) 97 | 98 | let _ = TipSee.TipItem(ID: "2", pointTo: xView!, contentView: UIView(),bubbleOptions: TipSee.Options.Bubble.default().with{$0.backgroundColor = .blue}) 99 | 100 | let _ = TipSee.TipItem(ID: "2", pointTo: xView!, contentView: UIView(),bubbleOptions: TipSee.Options.Bubble.default().with{$0.backgroundColor = .blue}) 101 | 102 | let count2 = CFGetRetainCount(xView!) 103 | xView = nil 104 | // then 105 | XCTAssertEqual(count2, count) 106 | XCTAssertNil(xView) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Tests/TipSeeTests/TipSeeManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipSeeManagerTests.swift 3 | // TipSee_Example 4 | // 5 | // Created by Adam Law on 01/03/2020. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import TipSee 11 | 12 | class TipSeeManagerTests: XCTestCase { 13 | 14 | private let window = UIWindow() 15 | private lazy var sut: TipSeeManager = { 16 | return TipSeeManager(on: window, with: TipSee.Options.default()) 17 | }() 18 | 19 | override func setUp() { 20 | super.setUp() 21 | XCTAssertNil(sut.currentIndex) 22 | XCTAssertTrue(sut.tips.isEmpty) 23 | } 24 | 25 | func test_addTip() { 26 | sut.add(new: TestData.tipItem1) 27 | XCTAssertEqual(sut.tips.count, 1) 28 | } 29 | 30 | func test_addTip_withAttributedTextAndBubbleOptions() { 31 | let text = NSAttributedString.init(string: "Tip message") 32 | sut.add(new: TestData.target, text: text, with: TestData.bubbleOptions) 33 | XCTAssertEqual(sut.tips.count, 1) 34 | } 35 | 36 | func test_addTip_withBubbleOptions() { 37 | sut.add(new: TestData.target, text: "Tip message", with: TestData.bubbleOptions) 38 | XCTAssertEqual(sut.tips.count, 1) 39 | } 40 | 41 | func test_addTips_withBubbleOptions() { 42 | sut.add( 43 | new: TestData.target, 44 | texts: ["Tip message 1", "Tip message 2"], 45 | with: TestData.bubbleOptions 46 | ) { previousButton, nextButton in } 47 | 48 | XCTAssertEqual(sut.tips.count, 1) 49 | } 50 | 51 | func test_nextTip_withTwoTips() { 52 | sut.add(new: TestData.tipItem1) 53 | sut.add(new: TestData.tipItem2) 54 | 55 | sut.next() 56 | XCTAssertEqual(sut.currentIndex, 0) 57 | XCTAssertEqual(sut.latestTip, TestData.tipItem1) 58 | 59 | sut.next() 60 | XCTAssertEqual(sut.currentIndex, 1) 61 | XCTAssertEqual(sut.latestTip, TestData.tipItem2) 62 | } 63 | 64 | func test_nextTip_withNoTips() { 65 | sut.next() 66 | XCTAssertNil(sut.currentIndex) 67 | XCTAssertNil(sut.latestTip) 68 | } 69 | 70 | func test_previousTip_withTwoTips() { 71 | sut.add(new: TestData.tipItem1) 72 | sut.next() 73 | sut.add(new: TestData.tipItem2) 74 | sut.next() 75 | 76 | sut.previous() 77 | XCTAssertEqual(sut.currentIndex, 0) 78 | XCTAssertEqual(sut.latestTip, TestData.tipItem1) 79 | 80 | sut.previous() 81 | XCTAssertEqual(sut.currentIndex, 0) 82 | XCTAssertEqual(sut.latestTip, TestData.tipItem1) 83 | } 84 | 85 | func test_previousTip_withNoTips() { 86 | sut.previous() 87 | XCTAssertNil(sut.currentIndex) 88 | XCTAssertNil(sut.latestTip) 89 | 90 | sut.finish() 91 | } 92 | 93 | private struct TestData { 94 | static let target = SimpleTipTarget(on: CGRect.zero, cornerRadius: 0) 95 | static let bubbleOptions = TipSee.Options.Bubble.default() 96 | static let tipItem1 = TipSee.TipItem(pointTo: target, contentView: UIView()) 97 | static let tipItem2 = TipSee.TipItem(pointTo: target, contentView: UIView()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/TipSeeTests/TipSeeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipSeeTests.swift 3 | // TipSee_Example 4 | // 5 | // Created by Farshad Jahanmanesh on 7/19/19. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import TipSee 11 | 12 | class TipSeeTests: XCTestCase { 13 | var sut : TipSee! 14 | var window : UIWindow! 15 | override func setUp() { 16 | super.setUp() 17 | window = UIWindow() 18 | sut = TipSee(on: window) 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | override func tearDown() { 23 | sut = nil 24 | window = nil 25 | super.tearDown() 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testShownItemConfig(){ 30 | let container = UIView() 31 | let targetView = UIView() 32 | container.addSubview(targetView) 33 | // given 34 | let item = sut.createItem(for: targetView,text:"hi",with: TipSee.Options.Bubble.default().with{$0.backgroundColor = .green}) 35 | 36 | // when 37 | let givenItem = sut.show(item: item) 38 | 39 | // then 40 | XCTAssertEqual(givenItem.bubbleOptions!.backgroundColor , .green) 41 | } 42 | 43 | func testShownBubbleContent(){ 44 | let container = UIView() 45 | let targetView = UIView() 46 | container.addSubview(targetView) 47 | // given 48 | let item = sut.createItem(for: targetView,text:"hi",with: TipSee.Options.Bubble.default().with{$0.backgroundColor = .green}) 49 | 50 | // when 51 | let givenItem = sut.show(item: item) 52 | 53 | // then 54 | XCTAssertNotNil(givenItem.contentView as? UILabel) 55 | } 56 | 57 | func testShownBubbleContentText(){ 58 | let container = UIView() 59 | let targetView = UIView() 60 | container.addSubview(targetView) 61 | // given 62 | let item = sut.createItem(for: targetView,text:"hi",with: TipSee.Options.Bubble.default().with{$0.backgroundColor = .green}) 63 | 64 | // when 65 | let givenItem = sut.show(item: item) 66 | 67 | // then 68 | XCTAssert((givenItem.contentView as? UILabel)?.text == "hi") 69 | } 70 | 71 | func testShownDismissItem(){ 72 | let container = UIView() 73 | let targetView = UIView() 74 | container.addSubview(targetView) 75 | // given 76 | let item = sut.createItem(for: targetView,text:"hi",with: TipSee.Options.Bubble.default().with{$0.backgroundColor = .green}) 77 | 78 | // when 79 | sut.show(item: item) 80 | 81 | // then 82 | sut.dismiss(item: item) 83 | XCTAssert(sut.subviews.count == 0) 84 | } 85 | 86 | func testNoOrderDismiss() { 87 | let container = UIView() 88 | let targetView = UIView() 89 | container.addSubview(targetView) 90 | // given 91 | let item = sut.createItem(for: targetView,text:"hi",with: TipSee.Options.Bubble.default().with{$0.backgroundColor = .green}) 92 | let item2 = sut.createItem(for: targetView,text:"hi",with: TipSee.Options.Bubble.default().with{$0.backgroundColor = .green}) 93 | 94 | // when 95 | sut.show(item: item) 96 | sut.show(item: item2) 97 | // then 98 | sut.dismiss(item: item2) 99 | sut.dismiss(item: item2) 100 | 101 | XCTAssert(sut.subviews.count == 1) 102 | } 103 | 104 | func testItemMemoryLeak(){ 105 | let container = UIView() 106 | let targetView = UIView() 107 | container.addSubview(targetView) 108 | // given 109 | let item = sut.createItem(for: targetView,text:"hi",with: TipSee.Options.Bubble.default().with{$0.backgroundColor = .green}) 110 | var item2 : TipSee.TipItem? = sut.createItem(for: targetView,text:"hi",with: TipSee.Options.Bubble.default().with{$0.backgroundColor = .green}) 111 | 112 | // when 113 | sut.show(item: item) 114 | sut.show(item: item2!) 115 | // then 116 | 117 | item2 = nil 118 | XCTAssertNil(item2) 119 | } 120 | 121 | public var options: TipSee.Options = TipSee.Options.default() 122 | private func findBetterSpace(view: TipTarget, preferredPosition: UIRectEdge?) -> UIRectEdge { 123 | let reletivePosition = view.tipFrame 124 | 125 | var edges = [(UIRectEdge, Bool)]() 126 | 127 | var left = options.safeAreaInsets.left + options.bubbles.padding.right + 16 128 | 129 | var right = UIScreen.main.bounds.width - (options.safeAreaInsets.right + options.bubbles.padding.left + 16) 130 | 131 | var top = options.safeAreaInsets.top + options.bubbles.padding.bottom + 16 132 | 133 | var bottom = UIScreen.main.bounds.height - (options.safeAreaInsets.bottom + options.bubbles.padding.top + 16) 134 | if #available(iOS 11.0, *) { 135 | bottom = UIScreen.main.bounds.height - (options.safeAreaInsets.bottom + options.bubbles.padding.top + 16) 136 | 137 | top = options.safeAreaInsets.top + options.bubbles.padding.bottom + 16 138 | 139 | right = UIScreen.main.bounds.width - (options.safeAreaInsets.right + options.bubbles.padding.left + 16) 140 | 141 | left = options.safeAreaInsets.left + options.bubbles.padding.right + 16 142 | } 143 | 144 | edges.append((.left, reletivePosition.minX > left)) 145 | 146 | edges.append((.top, reletivePosition.minY > top)) 147 | 148 | edges.append((.right, reletivePosition.maxX < right)) 149 | 150 | edges.append((.bottom, reletivePosition.maxY < bottom)) 151 | 152 | guard let doIHaveEnoughSpace = edges.first(where: {$0.0 == preferredPosition ?? options.defaultBubblePosition}), doIHaveEnoughSpace.1 else { 153 | return edges.first(where: {$0.0 != preferredPosition ?? options.defaultBubblePosition && $0.1})?.0 ?? options.defaultBubblePosition 154 | } 155 | return preferredPosition ?? options.defaultBubblePosition 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Tests/TipSeeTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(TipSeeTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /TipSee.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint TipSee.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.version = '1.6.3' 11 | s.name = 'TipSee' 12 | s.module_name = 'TipSee' 13 | s.summary = 'A lightweight, highly customizable tip / hint library for Swift' 14 | s.swift_version = '5.0' 15 | 16 | s.description = <<-DESC 17 | TipSee is a lightweight and highly customizable library that helps you to show beautiful tips and hints. 18 | DESC 19 | 20 | s.homepage = 'https://github.com/farshadjahanmanesh/TipSee' 21 | s.screenshots = 'https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_1.png','https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_2.png', 'https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_3.png', 'https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_4.png', 'https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_5.png','https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC_6.png','https://github.com/farshadjahanmanesh/tipC/raw/master/Example/images/TipC.gif' 22 | s.license = { :type => 'MIT', :file => 'LICENSE' } 23 | s.author = { 'farshadjahanmanesh' => 'farshadjahanmanesh@gmail.com' } 24 | s.source = { :git => 'https://github.com/farshadjahanmanesh/TipSee', :tag => s.version.to_s } 25 | s.social_media_url = 'https://twitter.com/' 26 | s.ios.deployment_target = '9.0' 27 | s.source_files = 'Sources/**/*.swift' 28 | s.frameworks = 'UIKit' 29 | end 30 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj --------------------------------------------------------------------------------