├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── question.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── Example Apps ├── iOS Example │ ├── App │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ └── Base.lproj │ │ │ └── LaunchScreen.storyboard │ ├── Models │ │ └── IndicatorPresetModel.swift │ ├── Info.plist │ └── Scenes │ │ └── PresetsController.swift ├── tvOS Example │ ├── App │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── App Icon & Top Shelf Image.brandassets │ │ │ │ ├── App Icon.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Middle.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ │ ├── App Icon - App Store.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Middle.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ │ ├── Top Shelf Image.imageset │ │ │ │ └── Contents.json │ │ │ │ ├── Top Shelf Image Wide.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── SPIndicator.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── swiftpm │ │ │ └── Package.resolved │ ├── xcuserdata │ │ └── ivanvorobei.xcuserdatad │ │ │ ├── xcdebugger │ │ │ └── Breakpoints_v2.xcbkptlist │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── iOS Example.xcscheme │ └── project.pbxproj └── AppImport │ ├── Package.swift │ └── Sources │ └── AppImport │ └── Example.swift ├── .gitignore ├── TODO.md ├── Package.swift ├── SPIndicator.podspec ├── CONTRIBUTING.md ├── LICENSE ├── Sources └── SPIndicator │ ├── Models │ ├── SPIndicatorPresentSide.swift │ └── SPIndicatorLayout.swift │ ├── Protocols │ └── SPIndicatorIconAnimatable.swift │ ├── Extensions │ ├── UIFontExtension.swift │ ├── UILabelExtension.swift │ ├── UIColorExtension.swift │ └── SwiftUIExtension.swift │ ├── Services │ └── SPIndicatorHaptic.swift │ ├── Icons │ ├── SPIndicatorIconDoneView.swift │ └── SPAlertIconErrorView.swift │ ├── SPIndicator.swift │ ├── SPIndicatorIconPreset.swift │ └── SPIndicatorView.swift ├── CODE_OF_CONDUCT.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ivanvorobei, sparrowcode] 4 | -------------------------------------------------------------------------------- /Example Apps/iOS Example/App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SparrowKit 3 | 4 | @main 5 | class AppDelegate: SPAppWindowDelegate {} 6 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # osX files 2 | .DS_Store 3 | .Trashes 4 | 5 | # Swift Package Manager 6 | .swiftpm 7 | 8 | # User Interface 9 | *UserInterfaceState.xcuserstate 10 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example Apps/SPIndicator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example Apps/SPIndicator.xcodeproj/xcuserdata/ivanvorobei.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Here provided ideas or features which will be implemented soon. 4 | 5 | - Add grades for left and right side when text not fit, including scroll animation for label 6 | - Add tvOS version (left-top corner) & docs. 7 | - If appear more show like stack. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: ivanvorobei 7 | 8 | --- 9 | 10 | **Feature Description** 11 | Describe what functionality you want to see. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Something is not clear with the project 4 | title: '' 5 | labels: question 6 | assignees: ivanvorobei 7 | 8 | --- 9 | 10 | **Describe the Problem** 11 | A clear and concise description of what you want to do. 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Goal 2 | 3 | 4 | ## Checklist 5 | 6 | - [] Testing in compability platforms 7 | - [] Installed correct via `Swift Package Manager` and `Cocoapods` 8 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example Apps/SPIndicator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example Apps/SPIndicator.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: ivanvorobei 7 | --- 8 | 9 | **Details** 10 | - iOS Version [e.g. 15.2] 11 | - Framework Version [e.g. 1.0.2] 12 | - Installed via [e.g. SPM, Cocoapods, Manually] 13 | 14 | **Describe the Bug** 15 | A clear and concise description of what the bug is. 16 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SPIndicator", 7 | platforms: [ 8 | .iOS(.v12), .tvOS(.v12) 9 | ], 10 | products: [ 11 | .library( 12 | name: "SPIndicator", 13 | targets: ["SPIndicator"] 14 | ) 15 | ], 16 | dependencies: [], 17 | targets: [ 18 | .target( 19 | name: "SPIndicator", 20 | swiftSettings: [ 21 | .define("SPINDICATOR_SPM") 22 | ] 23 | ) 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /SPIndicator.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = 'SPIndicator' 4 | s.version = '1.6.4' 5 | s.summary = 'Floating indicator, mimicrate to indicator which appear when silent mode switched. Can be present from top and bottom.' 6 | s.homepage = 'https://github.com/ivanvorobei/SPIndicator' 7 | s.source = { :git => 'https://github.com/ivanvorobei/SPIndicator.git', :tag => s.version } 8 | s.license = { :type => "MIT", :file => "LICENSE" } 9 | s.author = { "Ivan Vorobei" => "hello@ivanvorobei.io" } 10 | 11 | s.swift_version = '5.1' 12 | s.ios.deployment_target = '12.0' 13 | s.tvos.deployment_target = '12.0' 14 | 15 | s.source_files = 'Sources/SPIndicator/**/*.swift' 16 | 17 | end 18 | -------------------------------------------------------------------------------- /Example Apps/SPIndicator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SparrowKit", 6 | "repositoryURL": "https://github.com/ivanvorobei/SparrowKit", 7 | "state": { 8 | "branch": null, 9 | "revision": "f872b45a3e6aa5e2833ac8d3050a3f026d97ce0b", 10 | "version": "3.5.2" 11 | } 12 | }, 13 | { 14 | "package": "SPDiffable", 15 | "repositoryURL": "https://github.com/ivanvorobei/SPDiffable", 16 | "state": { 17 | "branch": null, 18 | "revision": "096ffb560906f762db4798ffa932d1c116ec8e9d", 19 | "version": "4.0.1" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Here provided more info about project, contribution process and recomended changes. 4 | Please, read it before pull request or create issue. 5 | 6 | ## Codestyle 7 | 8 | ### Marks 9 | 10 | For clean struct code good is using marks. 11 | 12 | ```swift 13 | class Example { 14 | 15 | // MARK: - Init 16 | 17 | init() {} 18 | } 19 | ``` 20 | 21 | Here you find all which using in project: 22 | 23 | - // MARK: - Init 24 | - // MARK: - Lifecycle 25 | - // MARK: - Layout 26 | - // MARK: - Public 27 | - // MARK: - Private 28 | - // MARK: - Internal 29 | - // MARK: - Models 30 | - // MARK: - Ovveride 31 | 32 | If you can't find valid, add new to codestyle agreements please. Other can be use if class is large and need struct it even without adding to codestyle agreements. 33 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example Apps/AppImport/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.4 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AppImport", 7 | platforms: [ 8 | .iOS(.v12), 9 | .tvOS(.v12), 10 | .watchOS(.v3) 11 | ], 12 | products: [ 13 | .library(name: "AppImport", targets: ["AppImport"]), 14 | ], 15 | dependencies: [ 16 | .package(name: "SparrowKit", url: "https://github.com/ivanvorobei/SparrowKit", .upToNextMajor(from: "3.5.1")), 17 | .package(name: "SPDiffable", url: "https://github.com/ivanvorobei/SPDiffable", .upToNextMajor(from: "4.0.1")), 18 | .package(name: "SPIndicator", path: "SPIndicator") 19 | ], 20 | targets: [ 21 | .target( 22 | name: "AppImport", 23 | dependencies: [ 24 | .product(name: "SparrowKit", package: "SparrowKit"), 25 | .product(name: "SPDiffable", package: "SPDiffable"), 26 | .product(name: "SPIndicator", package: "SPIndicator") 27 | ] 28 | ), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | arm64 28 | 29 | UIUserInterfaceStyle 30 | Automatic 31 | 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ivan Vorobei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Example Apps/SPIndicator.xcodeproj/xcuserdata/ivanvorobei.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | iOS Example.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | tvOS Example.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 3 16 | 17 | watchOS Example.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 3 21 | 22 | 23 | SuppressBuildableAutocreation 24 | 25 | F43F827A26578D83001D9B3D 26 | 27 | primary 28 | 29 | 30 | F43F82C026578F22001D9B3D 31 | 32 | primary 33 | 34 | 35 | F43F82EE26578FAB001D9B3D 36 | 37 | primary 38 | 39 | 40 | F43F8319265791A0001D9B3D 41 | 42 | primary 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example Apps/AppImport/Sources/AppImport/Example.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2020 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Models/SPIndicatorPresentSide.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | /** 25 | SPIndicator: Cases from preset styles. 26 | */ 27 | public enum SPIndicatorPresentSide { 28 | 29 | case top 30 | case center 31 | case bottom 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Protocols/SPIndicatorIconAnimatable.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | /** 25 | SPIndicator: Protocol for animatable views. 26 | */ 27 | public protocol SPIndicatorIconAnimatable { 28 | 29 | /** 30 | SPIndicator: Shoud call when need start animation. 31 | */ 32 | func animate() 33 | } 34 | -------------------------------------------------------------------------------- /Example Apps/iOS Example/Models/IndicatorPresetModel.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SPIndicator 24 | 25 | /** 26 | Example wrapper model. 27 | */ 28 | struct IndicatorPresetModel { 29 | 30 | var name: String 31 | var title: String 32 | var message: String? 33 | var preset: SPIndicatorIconPreset? 34 | 35 | var id: String { 36 | return name 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example Apps/iOS Example/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | 25 | @UIApplicationMain 26 | class AppDelegate: SPAppWindowDelegate { 27 | 28 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 29 | let rootController = PresetsController().wrapToNavigationController(prefersLargeTitles: false) 30 | makeKeyAndVisible(viewController: rootController, tint: .systemBlue) 31 | return true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Extensions/UIFontExtension.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | extension UIFont { 25 | 26 | static func preferredFont(forTextStyle style: TextStyle, weight: Weight, addPoints: CGFloat = 0) -> UIFont { 27 | let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) 28 | let font = UIFont.systemFont(ofSize: descriptor.pointSize + addPoints, weight: weight) 29 | let metrics = UIFontMetrics(forTextStyle: style) 30 | return metrics.scaledFont(for: font) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example Apps/iOS Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | SPIndicator 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSupportsIndirectInputEvents 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Models/SPIndicatorLayout.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | /** 25 | SPIndicator: Layout model for view. 26 | */ 27 | open class SPIndicatorLayout { 28 | 29 | /** 30 | SPIndicator: Icon size. 31 | */ 32 | open var iconSize: CGSize 33 | 34 | /** 35 | SPIndicator: Alert margings for each side. 36 | */ 37 | open var margins: UIEdgeInsets 38 | 39 | public init(iconSize: CGSize, margins: UIEdgeInsets) { 40 | self.iconSize = iconSize 41 | self.margins = margins 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Example Apps/iOS Example/App/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Extensions/UILabelExtension.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | extension UILabel { 25 | 26 | func layoutDynamicHeight(width: CGFloat) { 27 | layoutDynamicHeight(x: frame.origin.x, y: frame.origin.y, width: width) 28 | } 29 | 30 | func layoutDynamicHeight(x: CGFloat, y: CGFloat, width: CGFloat) { 31 | frame = CGRect.init(x: x, y: y, width: width, height: frame.height) 32 | sizeToFit() 33 | if frame.width != width { 34 | frame = .init(x: x, y: y, width: width, height: frame.height) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Example Apps/tvOS Example/App/LaunchScreen.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 | -------------------------------------------------------------------------------- /Example Apps/iOS Example/App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Services/SPIndicatorHaptic.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | /** 25 | SPIndicator: Wrapper of haptic styles. 26 | */ 27 | public enum SPIndicatorHaptic { 28 | 29 | case success 30 | case warning 31 | case error 32 | case none 33 | 34 | func impact() { 35 | #if os(iOS) 36 | let generator = UINotificationFeedbackGenerator() 37 | switch self { 38 | case .success: 39 | generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.success) 40 | case .warning: 41 | generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.warning) 42 | case .error: 43 | generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.error) 44 | case .none: 45 | break 46 | } 47 | #endif 48 | } 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Icons/SPIndicatorIconDoneView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | public class SPIndicatorIconDoneView: UIView, SPIndicatorIconAnimatable { 25 | 26 | public func animate() { 27 | let length = frame.width 28 | let animatablePath = UIBezierPath() 29 | animatablePath.move(to: CGPoint(x: length * 0.196, y: length * 0.527)) 30 | animatablePath.addLine(to: CGPoint(x: length * 0.47, y: length * 0.777)) 31 | animatablePath.addLine(to: CGPoint(x: length * 0.99, y: length * 0.25)) 32 | 33 | let animatableLayer = CAShapeLayer() 34 | animatableLayer.path = animatablePath.cgPath 35 | animatableLayer.fillColor = UIColor.clear.cgColor 36 | animatableLayer.strokeColor = tintColor?.cgColor 37 | animatableLayer.lineWidth = 4 38 | animatableLayer.lineCap = .round 39 | animatableLayer.lineJoin = .round 40 | animatableLayer.strokeEnd = 0 41 | layer.addSublayer(animatableLayer) 42 | 43 | let animation = CABasicAnimation(keyPath: "strokeEnd") 44 | animation.duration = 0.25 45 | animation.fromValue = 0 46 | animation.toValue = 1 47 | animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) 48 | animatableLayer.strokeEnd = 1 49 | animatableLayer.add(animation, forKey: "animation") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Example Apps/SPIndicator.xcodeproj/xcshareddata/xcschemes/iOS Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Extensions/UIColorExtension.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | extension UIColor { 25 | 26 | enum Compability { 27 | 28 | static var systemBackground: UIColor { 29 | #if os(iOS) 30 | if #available(iOS 13.0, *) { 31 | return UIColor.systemBackground 32 | } 33 | #endif 34 | return UIColor.white 35 | } 36 | 37 | static var secondarySystemBackground: UIColor { 38 | #if os(iOS) 39 | if #available(iOS 13.0, *) { 40 | return UIColor.secondarySystemBackground 41 | } 42 | #endif 43 | return UIColor(red: 242/255, green: 242/255, blue: 247/255, alpha: 1) 44 | } 45 | 46 | static var separator: UIColor { 47 | if #available(iOS 13.0, tvOS 13.0, *) { 48 | return UIColor.separator 49 | } else { 50 | return UIColor(red: 60/255, green: 60/255, blue: 67/255, alpha: 1) 51 | } 52 | } 53 | 54 | static var label: UIColor { 55 | if #available(iOS 13.0, tvOS 13.0, *) { 56 | return UIColor.label 57 | } else { 58 | return UIColor.black 59 | } 60 | } 61 | 62 | static var secondaryLabel: UIColor { 63 | if #available(iOS 13.0, tvOS 13.0, *) { 64 | return UIColor.secondaryLabel 65 | } else { 66 | return UIColor(red: 138/255, green: 138/255, blue: 142/255, alpha: 1) 67 | } 68 | } 69 | } 70 | 71 | static var buttonArea: UIColor { 72 | if #available(iOS 13.0, tvOS 13.0, *) { 73 | return UIColor { (traits) -> UIColor in 74 | return traits.userInterfaceStyle == .dark ? UIColor(red: 61/255, green: 62/255, blue: 66/255, alpha: 1) : 75 | UIColor(red: 238/255, green: 238/255, blue: 240/255, alpha: 1) 76 | } 77 | } else { 78 | return UIColor(red: 238/255, green: 238/255, blue: 240/255, alpha: 1) 79 | } 80 | } 81 | 82 | @available(iOSApplicationExtension, unavailable) 83 | static var tint: UIColor { 84 | get { 85 | let value = UIApplication.shared.windows.first?.tintColor 86 | guard let tint = value else { return .systemBlue } 87 | return tint 88 | } 89 | set { 90 | UIApplication.shared.windows.forEach({ $0.tintColor = newValue }) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Icons/SPAlertIconErrorView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | public class SPIndicatorIconErrorView: UIView, SPIndicatorIconAnimatable { 25 | 26 | public func animate() { 27 | animateTopToBottomLine() 28 | animateBottomToTopLine() 29 | } 30 | 31 | private func animateTopToBottomLine() { 32 | let length = frame.width 33 | 34 | let topToBottomLine = UIBezierPath() 35 | topToBottomLine.move(to: CGPoint(x: length * 0, y: length * 0)) 36 | topToBottomLine.addLine(to: CGPoint(x: length * 1, y: length * 1)) 37 | 38 | let animatableLayer = CAShapeLayer() 39 | animatableLayer.path = topToBottomLine.cgPath 40 | animatableLayer.fillColor = UIColor.clear.cgColor 41 | animatableLayer.strokeColor = tintColor?.cgColor 42 | animatableLayer.lineWidth = 4 43 | animatableLayer.lineCap = .round 44 | animatableLayer.lineJoin = .round 45 | animatableLayer.strokeEnd = 0 46 | self.layer.addSublayer(animatableLayer) 47 | 48 | let animation = CABasicAnimation(keyPath: "strokeEnd") 49 | animation.duration = 0.22 50 | animation.fromValue = 0 51 | animation.toValue = 1 52 | animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) 53 | 54 | animatableLayer.strokeEnd = 1 55 | animatableLayer.add(animation, forKey: "animation") 56 | } 57 | 58 | private func animateBottomToTopLine() { 59 | let length = frame.width 60 | 61 | let bottomToTopLine = UIBezierPath() 62 | bottomToTopLine.move(to: CGPoint(x: length * 0, y: length * 1)) 63 | bottomToTopLine.addLine(to: CGPoint(x: length * 1, y: length * 0)) 64 | 65 | let animatableLayer = CAShapeLayer() 66 | animatableLayer.path = bottomToTopLine.cgPath 67 | animatableLayer.fillColor = UIColor.clear.cgColor 68 | animatableLayer.strokeColor = tintColor?.cgColor 69 | animatableLayer.lineWidth = 4 70 | animatableLayer.lineCap = .round 71 | animatableLayer.lineJoin = .round 72 | animatableLayer.strokeEnd = 0 73 | self.layer.addSublayer(animatableLayer) 74 | 75 | let animation = CABasicAnimation(keyPath: "strokeEnd") 76 | animation.duration = 0.22 77 | animation.fromValue = 0 78 | animation.toValue = 1 79 | animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) 80 | 81 | animatableLayer.strokeEnd = 1 82 | animatableLayer.add(animation, forKey: "animation") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/SPIndicator/Extensions/SwiftUIExtension.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | #if os(iOS) 25 | 26 | @available(iOS 13.0, *) 27 | @available(iOSApplicationExtension, unavailable) 28 | 29 | extension View { 30 | 31 | public func SPIndicator( 32 | isPresent: Binding, 33 | indicatorView: SPIndicatorView, 34 | duration: TimeInterval = 2.0, 35 | haptic: SPIndicatorHaptic = .none 36 | ) -> some View { 37 | 38 | if isPresent.wrappedValue { 39 | let indicatorCompletion = indicatorView.completion 40 | let indicatorDismiss = { 41 | isPresent.wrappedValue = false 42 | indicatorCompletion?() 43 | } 44 | indicatorView.duration = duration 45 | indicatorView.present(haptic: haptic, completion: indicatorDismiss) 46 | } 47 | return self 48 | } 49 | 50 | public func SPIndicator( 51 | isPresent: Binding, 52 | title: String = "", 53 | message: String? = nil, 54 | duration: TimeInterval = 2.0, 55 | presentSide: SPIndicatorPresentSide = .top, 56 | dismissByDrag: Bool = true, 57 | preset: SPIndicatorIconPreset = .done, 58 | haptic: SPIndicatorHaptic = .none, 59 | layout: SPIndicatorLayout? = nil, 60 | completion: (()-> Void)? = nil 61 | ) -> some View { 62 | 63 | let indicatorView = SPIndicatorView(title: title, message: message, preset: preset) 64 | indicatorView.presentSide = presentSide 65 | indicatorView.dismissByDrag = dismissByDrag 66 | indicatorView.layout = layout ?? SPIndicatorLayout(for: preset) 67 | indicatorView.completion = completion 68 | return SPIndicator(isPresent: isPresent, indicatorView: indicatorView, duration: duration, haptic: haptic) 69 | } 70 | 71 | public func SPIndicator( 72 | isPresent: Binding, 73 | title: String, 74 | duration: TimeInterval = 2.0, 75 | presentSide: SPIndicatorPresentSide = .top, 76 | dismissByDrag: Bool = true, 77 | preset: SPIndicatorIconPreset = .done, 78 | haptic: SPIndicatorHaptic = .none, 79 | completion: (()-> Void)? = nil 80 | ) -> some View { 81 | 82 | let indicatorView = SPIndicatorView(title: title, preset: preset) 83 | indicatorView.presentSide = presentSide 84 | indicatorView.dismissByDrag = dismissByDrag 85 | indicatorView.completion = completion 86 | return SPIndicator(isPresent: isPresent, indicatorView: indicatorView, duration: duration, haptic: haptic) 87 | } 88 | 89 | } 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/SPIndicator/SPIndicator.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | @available(iOSApplicationExtension, unavailable) 25 | public enum SPIndicator { 26 | 27 | #if os(iOS) 28 | 29 | /** 30 | SPIndicator: Present alert with preset and custom haptic. 31 | 32 | - parameter title: Title text in alert. 33 | - parameter message: Subtitle text in alert. Optional. 34 | - parameter preset: Icon ready-use style or custom image. 35 | - parameter haptic: Haptic response with present. Default is `.success`. 36 | - parameter presentSide: Choose from side appear indicator. 37 | - parameter completion: Will call with dismiss alert. 38 | */ 39 | public static func present(title: String, message: String? = nil, preset: SPIndicatorIconPreset, haptic: SPIndicatorHaptic, from presentSide: SPIndicatorPresentSide = .top, completion: (() -> Void)? = nil) { 40 | let alertView = SPIndicatorView(title: title, message: message, preset: preset) 41 | alertView.presentSide = presentSide 42 | alertView.present(haptic: haptic, completion: completion) 43 | } 44 | 45 | /** 46 | SPIndicator: Present alert with preset and automatically detect type haptic. 47 | 48 | - parameter title: Title text in alert. 49 | - parameter message: Subtitle text in alert. Optional. 50 | - parameter preset: Icon ready-use style or custom image. 51 | - parameter presentSide: Choose from side appear indicator. Default is `.top`. 52 | - parameter completion: Will call with dismiss alert. 53 | */ 54 | public static func present(title: String, message: String? = nil, preset: SPIndicatorIconPreset, from presentSide: SPIndicatorPresentSide = .top, completion: (() -> Void)? = nil) { 55 | let alertView = SPIndicatorView(title: title, message: message, preset: preset) 56 | alertView.presentSide = presentSide 57 | let haptic = preset.getHaptic() 58 | alertView.present(haptic: haptic, completion: completion) 59 | } 60 | 61 | /** 62 | SPIndicator: Show only message, without title and icon. 63 | 64 | - parameter message: Title text. 65 | - parameter haptic: Haptic response with present. Default is `.success`. 66 | - parameter presentSide: Choose from side appear indicator. Default is `.top`. 67 | - parameter completion: Will call with dismiss alert. 68 | */ 69 | public static func present(title: String, message: String? = nil, haptic: SPIndicatorHaptic, from presentSide: SPIndicatorPresentSide = .top, completion: (() -> Void)? = nil) { 70 | let alertView = SPIndicatorView(title: title, message: message) 71 | alertView.presentSide = presentSide 72 | alertView.present(haptic: haptic, completion: completion) 73 | } 74 | 75 | #endif 76 | } 77 | -------------------------------------------------------------------------------- /Sources/SPIndicator/SPIndicatorIconPreset.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | /** 25 | SPIndicator: Represent icon wrapper. 26 | Included default styles and can be custom image. 27 | */ 28 | public enum SPIndicatorIconPreset { 29 | 30 | case done 31 | case error 32 | case spin(_ style: UIActivityIndicatorView.Style) 33 | case custom(_ image: UIImage) 34 | } 35 | 36 | // Get view and haptic by Preset. 37 | 38 | public extension SPIndicatorIconPreset { 39 | 40 | func createView() -> UIView { 41 | switch self { 42 | case .done: 43 | let view = SPIndicatorIconDoneView() 44 | return view 45 | case .error: 46 | let view = SPIndicatorIconErrorView() 47 | view.tintColor = UIColor.systemRed 48 | return view 49 | case .spin(let style): 50 | let view = UIActivityIndicatorView(style: style) 51 | view.startAnimating() 52 | return view 53 | case .custom(let image): 54 | let imageView = UIImageView(image: image) 55 | imageView.contentMode = .scaleAspectFit 56 | return imageView 57 | } 58 | } 59 | 60 | func getHaptic() -> SPIndicatorHaptic { 61 | switch self { 62 | case .error: return .error 63 | case .done: return .success 64 | case .spin(_): return .none 65 | case .custom(_): return .success 66 | } 67 | } 68 | } 69 | 70 | // Get layout by preset. 71 | 72 | public extension SPIndicatorLayout { 73 | 74 | convenience init() { 75 | self.init( 76 | iconSize: .init( 77 | width: Self.defaultIconSideSize, 78 | height: Self.defaultIconSideSize 79 | ), 80 | margins: .init( 81 | top: Self.defaultVerticallInset, 82 | left: Self.defaultHorizontalInset, 83 | bottom: Self.defaultVerticallInset, 84 | right: Self.defaultHorizontalInset 85 | ) 86 | ) 87 | } 88 | 89 | static func message() -> SPIndicatorLayout { 90 | let layout = SPIndicatorLayout() 91 | return layout 92 | } 93 | 94 | convenience init(for preset: SPIndicatorIconPreset) { 95 | switch preset { 96 | case .done: 97 | self.init() 98 | iconSize = .init(width: 24, height: 24) 99 | case .error: 100 | self.init() 101 | iconSize = .init(width: 14, height: 14) 102 | margins.left = 19 103 | margins.right = margins.left 104 | case .spin(_): 105 | self.init() 106 | case .custom(_): 107 | self.init() 108 | } 109 | } 110 | 111 | // Default values. 112 | 113 | private static var defaultIconSideSize: CGFloat { 28 } 114 | private static var defaultSpaceBetweenIconAndTitle: CGFloat { 26 } 115 | private static var defaultVerticallInset: CGFloat { 8 } 116 | private static var defaultHorizontalInset: CGFloat { 15 } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | hello@ivanvorobei.io. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SPIndicator 2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 | Mimicrate for the indicator which appears when silent mode is turned on / off. Availabe 2 animated presets: `done` & `error`. Also supports custom images and presentations from top, center & bottom sides. 10 | 11 | To get an alert like in Apple music, use this library instead [SPAlert](https://github.com/ivanvorobei/SPAlert). 12 | 13 | ## Navigate 14 | 15 | - [Installation](#installation) 16 | - [Swift Package Manager](#swift-package-manager) 17 | - [CocoaPods](#cocoapods) 18 | - [Manually](#manually) 19 | - [Quick Start](#quick-start) 20 | - [Usage](#usage) 21 | - [Duration](#duration) 22 | - [Layout](#layout) 23 | - [Dismiss by Drag](#dismiss-by-drag) 24 | - [Haptic](#haptic) 25 | - [Present Side](#present-side) 26 | - [Shared Configuration](#shared-configuration) 27 | - [Swift UI](#swiftui) 28 | - [Russian Community](#russian-community) 29 | 30 | ## Installation 31 | 32 | Ready for use on iOS 12+ & tvOS 12+. 33 | 34 | ### Swift Package Manager 35 | 36 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. 37 | 38 | Once you have your Swift package set up, adding as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 39 | 40 | ```swift 41 | dependencies: [ 42 | .package(url: "https://github.com/ivanvorobei/SPIndicator", .upToNextMajor(from: "1.6.0")) 43 | ] 44 | ``` 45 | 46 | ### CocoaPods: 47 | 48 | [CocoaPods](https://cocoapods.org) is a dependency manager. For usage and installation instructions, visit their website. To integrate using CocoaPods, specify it in your `Podfile`: 49 | 50 | ```ruby 51 | pod 'SPIndicator' 52 | ``` 53 | 54 | ### Manually 55 | 56 | If you prefer not to use any of dependency managers, you can integrate manually. Put `Sources/SPIndicator` folder in your Xcode project. Make sure to enable `Copy items if needed` and `Create groups`. 57 | 58 | ## Quick Start 59 | 60 | For best experience, I recommend presenting indicator by calling the class functions `SPIndicator`. These functions are updated regularly and show the indicator as Apple way: 61 | 62 | ```swift 63 | // Presets: 64 | SPIndicator.present(title: "Error", message: "Try Again", preset: .error) 65 | 66 | // For show with custom image: 67 | let image = UIImage.init(systemName: "sun.min.fill")!.withTintColor(.systemYellow, renderingMode: .alwaysOriginal) 68 | SPIndicator.present(title: "Custom Image", message: "With tint color", preset: .custom(image)) 69 | 70 | // For show text only: 71 | SPIndicator.present(title: "Error", haptic: .error) 72 | ``` 73 | 74 | ## Usage 75 | 76 | ### Duration 77 | 78 | To change the presentation duration time, create the indicator view and call the method `present` with a custom duration: 79 | 80 | ```swift 81 | let indicatorView = SPIndicatorView(title: "Complete", preset: .done) 82 | indicatorView.present(duration: 3) 83 | ``` 84 | 85 | ### Layout 86 | 87 | To customise the layout & margins use the `layout` property. You can manage margins for each side, icon size and space between image and titles: 88 | 89 | ```swift 90 | indicatorView.layout.iconSize = .init(width: 24, height: 24) 91 | indicatorView.layout.margins.top = 12 92 | ``` 93 | 94 | ### Dismiss by Drag 95 | 96 | By default, you can drag the indicator to hide it. While the indicator is dragging, dismiss is not working. This behaviour can be disabled: 97 | 98 | ```swift 99 | indicatorView.dismissByDrag = false 100 | ``` 101 | 102 | ### Haptic 103 | 104 | To manage haptics, you shoud pass it in present method: 105 | 106 | ```swift 107 | indicatorView.present(duration: 1.5, haptic: .success, completion: nil) 108 | ``` 109 | 110 | You can remove the duration and completion parameters from the init. They have default values. 111 | 112 | ### Present Side 113 | 114 | You can change the presentation side: 115 | 116 | ```swift 117 | SPIndicator.present(title: "Error", message: "Try Again", preset: .error, from: .bottom) 118 | 119 | // or for custom `SPIndicatorView` 120 | 121 | indicatorView.presentSide = .bottom 122 | ``` 123 | In the case from above, the indicator will appear from bottom and will be attached to bottom. To manage the offset - check the property `offset`. 124 | 125 | ### Shared Configuration 126 | 127 | Also, you can change some default values for the alerts. For example, you can change the default duration for an alert with this code: 128 | 129 | ```swift 130 | SPIndicatorView.appearance().duration = 2 131 | ``` 132 | 133 | It will apply for all alerts. I recommend to set it in app delegate, but you can change it in runtime. 134 | 135 | ## SwiftUI 136 | 137 | Use like system alert only show message tips: 138 | 139 | ```swift 140 | Button("Show Indicator") { 141 | showIndicator = true 142 | }.SPIndicator(isPresent: $showIndicator, title: "This is title only") 143 | ``` 144 | 145 | or show message, title, image and other configuration: 146 | 147 | ```swift 148 | Button("Show Indicator") { 149 | showIndicator = true 150 | }.SPIndicator( 151 | isPresent: $showIndicator, 152 | title: "Title", 153 | message: "Message", 154 | duration: 2.0, 155 | presentSide: .top, 156 | dismissByDrag: false, 157 | preset: .custom(UIImage(systemName: "heart")!), 158 | haptic: .success, 159 | layout: .init(), 160 | completion: { 161 | print("Indicator is destoryed") 162 | }) 163 | ``` 164 | 165 | ## Russian Community 166 | 167 | Я веду [телеграм-канал](https://sparrowcode.io/telegram), там публикую новости и туториалы.
168 | С проблемой помогут [в чате](https://sparrowcode.io/telegram/chat). 169 | 170 | Видео-туториалы выклыдываю на [YouTube](https://ivanvorobei.io/youtube): 171 | 172 | [![Tutorials on YouTube](https://cdn.ivanvorobei.io/github/readme/youtube-preview.jpg)](https://ivanvorobei.io/youtube) 173 | -------------------------------------------------------------------------------- /Example Apps/iOS Example/Scenes/PresetsController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | import SPDiffable 25 | import SPIndicator 26 | 27 | class PresetsController: SPDiffableTableController { 28 | 29 | // MARK: - Init 30 | 31 | init() { 32 | super.init(style: .insetGrouped) 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | // MARK: - Lifecycle 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | navigationItem.title = "SPIndicator Presets" 44 | 45 | currentPreset = presets.first! 46 | configureDiffable(sections: content, cellProviders: SPDiffableTableDataSource.CellProvider.default) 47 | 48 | let segmentedControl = UISegmentedControl(items: ["Top", "Center", "Bottom"]) 49 | navigationItem.titleView = segmentedControl 50 | segmentedControl.selectedSegmentIndex = 0 51 | 52 | navigationController?.isToolbarHidden = false 53 | toolbarItems = [ 54 | .init(image: .system("chevron.down.circle"), primaryAction: .init(handler: { (action) in 55 | guard let currentPreset = self.currentPreset else { 56 | self.currentPreset = self.presets.first 57 | return 58 | } 59 | guard let index = self.presets.firstIndex(where: { $0.id == currentPreset.id }) else { return } 60 | self.currentPreset = self.presets[safe: index + 1] 61 | if self.currentPreset == nil { 62 | self.currentPreset = self.presets.first 63 | } 64 | })), 65 | .init(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), 66 | .init(systemItem: .play, primaryAction: .init(handler: { [weak self] (action) in 67 | guard let self = self else { return } 68 | guard let preset = self.currentPreset else { return } 69 | guard let segmentControl = self.navigationItem.titleView as? UISegmentedControl else { return } 70 | 71 | let presentSide: SPIndicatorPresentSide = { 72 | switch segmentControl.selectedSegmentIndex { 73 | case 0: return SPIndicatorPresentSide.top 74 | case 1: return SPIndicatorPresentSide.center 75 | case 2: return SPIndicatorPresentSide.bottom 76 | default: fatalError() 77 | } 78 | }() 79 | 80 | if let iconPreset = preset.preset { 81 | SPIndicator.present(title: preset.title, message: preset.message, preset: iconPreset, from: presentSide, completion: nil) 82 | } else { 83 | SPIndicator.present(title: preset.title, message: preset.message, haptic: .success, from: presentSide) 84 | } 85 | 86 | }), menu: nil), 87 | .init(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), 88 | ] 89 | } 90 | 91 | // MARK: - Data 92 | 93 | fileprivate var presets: [IndicatorPresetModel] { 94 | return [ 95 | IndicatorPresetModel( 96 | name: "Done", 97 | title: "Done", 98 | message: "Animatable", 99 | preset: .done 100 | ), 101 | IndicatorPresetModel( 102 | name: "Error", 103 | title: "Oops", 104 | message: "Try again", 105 | preset: .error 106 | ), 107 | IndicatorPresetModel( 108 | name: "Spin", 109 | title: "Spin", 110 | message: "Animatable", 111 | preset: .spin(.medium) 112 | ), 113 | IndicatorPresetModel( 114 | name: "Custom Image", 115 | title: "Custom Image", 116 | message: "SFSymbols Image", 117 | preset: .custom(UIImage.init(systemName: "swift")!) 118 | ), 119 | IndicatorPresetModel( 120 | name: "Specific Tint", 121 | title: "Specific Tint", 122 | message: "For any image", 123 | preset: .custom(UIImage.init(systemName: "sun.min.fill")!.withTintColor(.systemYellow, renderingMode: .alwaysOriginal)) 124 | ), 125 | IndicatorPresetModel( 126 | name: "Message", 127 | title: "Only Title", 128 | message: nil, 129 | preset: nil 130 | ), 131 | IndicatorPresetModel( 132 | name: "Message with Subtitle", 133 | title: "Title", 134 | message: "Subtitle", 135 | preset: nil 136 | ), 137 | IndicatorPresetModel( 138 | name: "Message Large Text with Icon", 139 | title: "You can read it later when you have time.", 140 | message: nil, 141 | preset: .custom(UIImage.init(systemName: "envelope.open.fill")!) 142 | ), 143 | IndicatorPresetModel( 144 | name: "Message Large Text without Icon", 145 | title: "You can read it later when you have time. Saved to Bookmarks", 146 | message: nil, 147 | preset: nil 148 | ) 149 | ] 150 | } 151 | 152 | // MARK: - Diffable 153 | 154 | var currentPreset: IndicatorPresetModel? { 155 | didSet { 156 | diffableDataSource?.set(self.content, animated: true, completion: nil) 157 | } 158 | } 159 | 160 | var content: [SPDiffableSection] { 161 | let items = presets.map { (preset) -> SPDiffableTableRow in 162 | return SPDiffableTableRow( 163 | text: preset.name, 164 | accessoryType: (preset.id == currentPreset?.id) ? .checkmark : .none, 165 | selectionStyle: .none) { [weak self] _, _ in 166 | guard let self = self else { return } 167 | self.currentPreset = preset 168 | } 169 | } 170 | return [ 171 | SPDiffableSection(id: "presets", header: nil, footer: nil, items: items) 172 | ] 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Example Apps/SPIndicator.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F43F829D26578DCA001D9B3D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F43F829426578DCA001D9B3D /* Assets.xcassets */; }; 11 | F43F829E26578DCA001D9B3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F43F829526578DCA001D9B3D /* LaunchScreen.storyboard */; }; 12 | F43F829F26578DCA001D9B3D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43F829726578DCA001D9B3D /* AppDelegate.swift */; }; 13 | F43F82A126578DCA001D9B3D /* PresetsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43F829B26578DCA001D9B3D /* PresetsController.swift */; }; 14 | F457C6AB276927C1005B4E19 /* AppImport in Frameworks */ = {isa = PBXBuildFile; productRef = F457C6AA276927C1005B4E19 /* AppImport */; }; 15 | F457C6AD27692A1B005B4E19 /* AppImport in Frameworks */ = {isa = PBXBuildFile; productRef = F457C6AC27692A1B005B4E19 /* AppImport */; }; 16 | F4B96F9C265AC36F00A01A29 /* IndicatorPresetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B96F9B265AC36F00A01A29 /* IndicatorPresetModel.swift */; }; 17 | F4BAA4402658297300DA5BF7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4BAA43D2658297300DA5BF7 /* LaunchScreen.storyboard */; }; 18 | F4BAA4412658297300DA5BF7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4BAA43E2658297300DA5BF7 /* Assets.xcassets */; }; 19 | F4BAA4422658297300DA5BF7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BAA43F2658297300DA5BF7 /* AppDelegate.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXCopyFilesBuildPhase section */ 23 | F43F82EA26578F24001D9B3D /* Embed Watch Content */ = { 24 | isa = PBXCopyFilesBuildPhase; 25 | buildActionMask = 2147483647; 26 | dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; 27 | dstSubfolderSpec = 16; 28 | files = ( 29 | ); 30 | name = "Embed Watch Content"; 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | /* End PBXCopyFilesBuildPhase section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | F43F827B26578D83001D9B3D /* iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | F43F829426578DCA001D9B3D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | F43F829626578DCA001D9B3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 39 | F43F829726578DCA001D9B3D /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 40 | F43F829B26578DCA001D9B3D /* PresetsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresetsController.swift; sourceTree = ""; }; 41 | F43F829C26578DCA001D9B3D /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | F43F82AB26578F03001D9B3D /* tvOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "tvOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | F43F82B926578F04001D9B3D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | F457C6A92769270D005B4E19 /* AppImport */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AppImport; sourceTree = ""; }; 45 | F46C34BC27071E43005681BA /* SPIndicator */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SPIndicator; path = ..; sourceTree = ""; }; 46 | F4B96F9B265AC36F00A01A29 /* IndicatorPresetModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndicatorPresetModel.swift; sourceTree = ""; }; 47 | F4BAA43D2658297300DA5BF7 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 48 | F4BAA43E2658297300DA5BF7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49 | F4BAA43F2658297300DA5BF7 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | F43F827826578D83001D9B3D /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | F457C6AB276927C1005B4E19 /* AppImport in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | F43F82A826578F03001D9B3D /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | F457C6AD27692A1B005B4E19 /* AppImport in Frameworks */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXFrameworksBuildPhase section */ 70 | 71 | /* Begin PBXGroup section */ 72 | F43F827226578D83001D9B3D = { 73 | isa = PBXGroup; 74 | children = ( 75 | F457C6A92769270D005B4E19 /* AppImport */, 76 | F46C34BC27071E43005681BA /* SPIndicator */, 77 | F43F829226578DCA001D9B3D /* iOS Example */, 78 | F43F82AC26578F03001D9B3D /* tvOS Example */, 79 | F43F827C26578D83001D9B3D /* Products */, 80 | F43F82A426578E31001D9B3D /* Frameworks */, 81 | ); 82 | sourceTree = ""; 83 | }; 84 | F43F827C26578D83001D9B3D /* Products */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | F43F827B26578D83001D9B3D /* iOS Example.app */, 88 | F43F82AB26578F03001D9B3D /* tvOS Example.app */, 89 | ); 90 | name = Products; 91 | sourceTree = ""; 92 | }; 93 | F43F829226578DCA001D9B3D /* iOS Example */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | F43F829326578DCA001D9B3D /* App */, 97 | F4B96F9A265AC36800A01A29 /* Models */, 98 | F43F829A26578DCA001D9B3D /* Scenes */, 99 | F43F829C26578DCA001D9B3D /* Info.plist */, 100 | ); 101 | path = "iOS Example"; 102 | sourceTree = ""; 103 | }; 104 | F43F829326578DCA001D9B3D /* App */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | F43F829726578DCA001D9B3D /* AppDelegate.swift */, 108 | F43F829426578DCA001D9B3D /* Assets.xcassets */, 109 | F43F829526578DCA001D9B3D /* LaunchScreen.storyboard */, 110 | ); 111 | path = App; 112 | sourceTree = ""; 113 | }; 114 | F43F829A26578DCA001D9B3D /* Scenes */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | F43F829B26578DCA001D9B3D /* PresetsController.swift */, 118 | ); 119 | path = Scenes; 120 | sourceTree = ""; 121 | }; 122 | F43F82A426578E31001D9B3D /* Frameworks */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | ); 126 | name = Frameworks; 127 | sourceTree = ""; 128 | }; 129 | F43F82AC26578F03001D9B3D /* tvOS Example */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | F4BAA43C2658297300DA5BF7 /* App */, 133 | F43F82B926578F04001D9B3D /* Info.plist */, 134 | ); 135 | path = "tvOS Example"; 136 | sourceTree = ""; 137 | }; 138 | F4B96F9A265AC36800A01A29 /* Models */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | F4B96F9B265AC36F00A01A29 /* IndicatorPresetModel.swift */, 142 | ); 143 | path = Models; 144 | sourceTree = ""; 145 | }; 146 | F4BAA43C2658297300DA5BF7 /* App */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | F4BAA43F2658297300DA5BF7 /* AppDelegate.swift */, 150 | F4BAA43D2658297300DA5BF7 /* LaunchScreen.storyboard */, 151 | F4BAA43E2658297300DA5BF7 /* Assets.xcassets */, 152 | ); 153 | path = App; 154 | sourceTree = ""; 155 | }; 156 | /* End PBXGroup section */ 157 | 158 | /* Begin PBXNativeTarget section */ 159 | F43F827A26578D83001D9B3D /* iOS Example */ = { 160 | isa = PBXNativeTarget; 161 | buildConfigurationList = F43F828F26578D84001D9B3D /* Build configuration list for PBXNativeTarget "iOS Example" */; 162 | buildPhases = ( 163 | F43F827726578D83001D9B3D /* Sources */, 164 | F43F827826578D83001D9B3D /* Frameworks */, 165 | F43F827926578D83001D9B3D /* Resources */, 166 | F43F82EA26578F24001D9B3D /* Embed Watch Content */, 167 | ); 168 | buildRules = ( 169 | ); 170 | dependencies = ( 171 | ); 172 | name = "iOS Example"; 173 | packageProductDependencies = ( 174 | F457C6AA276927C1005B4E19 /* AppImport */, 175 | ); 176 | productName = SparrowKit; 177 | productReference = F43F827B26578D83001D9B3D /* iOS Example.app */; 178 | productType = "com.apple.product-type.application"; 179 | }; 180 | F43F82AA26578F03001D9B3D /* tvOS Example */ = { 181 | isa = PBXNativeTarget; 182 | buildConfigurationList = F43F82BA26578F04001D9B3D /* Build configuration list for PBXNativeTarget "tvOS Example" */; 183 | buildPhases = ( 184 | F43F82A726578F03001D9B3D /* Sources */, 185 | F43F82A826578F03001D9B3D /* Frameworks */, 186 | F43F82A926578F03001D9B3D /* Resources */, 187 | ); 188 | buildRules = ( 189 | ); 190 | dependencies = ( 191 | ); 192 | name = "tvOS Example"; 193 | packageProductDependencies = ( 194 | F457C6AC27692A1B005B4E19 /* AppImport */, 195 | ); 196 | productName = "tvOS Example"; 197 | productReference = F43F82AB26578F03001D9B3D /* tvOS Example.app */; 198 | productType = "com.apple.product-type.application"; 199 | }; 200 | /* End PBXNativeTarget section */ 201 | 202 | /* Begin PBXProject section */ 203 | F43F827326578D83001D9B3D /* Project object */ = { 204 | isa = PBXProject; 205 | attributes = { 206 | LastSwiftUpdateCheck = 1250; 207 | LastUpgradeCheck = 1250; 208 | TargetAttributes = { 209 | F43F827A26578D83001D9B3D = { 210 | CreatedOnToolsVersion = 12.5; 211 | }; 212 | F43F82AA26578F03001D9B3D = { 213 | CreatedOnToolsVersion = 12.5; 214 | }; 215 | }; 216 | }; 217 | buildConfigurationList = F43F827626578D83001D9B3D /* Build configuration list for PBXProject "SPIndicator" */; 218 | compatibilityVersion = "Xcode 9.3"; 219 | developmentRegion = en; 220 | hasScannedForEncodings = 0; 221 | knownRegions = ( 222 | en, 223 | Base, 224 | ); 225 | mainGroup = F43F827226578D83001D9B3D; 226 | packageReferences = ( 227 | ); 228 | productRefGroup = F43F827C26578D83001D9B3D /* Products */; 229 | projectDirPath = ""; 230 | projectRoot = ""; 231 | targets = ( 232 | F43F827A26578D83001D9B3D /* iOS Example */, 233 | F43F82AA26578F03001D9B3D /* tvOS Example */, 234 | ); 235 | }; 236 | /* End PBXProject section */ 237 | 238 | /* Begin PBXResourcesBuildPhase section */ 239 | F43F827926578D83001D9B3D /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | F43F829D26578DCA001D9B3D /* Assets.xcassets in Resources */, 244 | F43F829E26578DCA001D9B3D /* LaunchScreen.storyboard in Resources */, 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | F43F82A926578F03001D9B3D /* Resources */ = { 249 | isa = PBXResourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | F4BAA4412658297300DA5BF7 /* Assets.xcassets in Resources */, 253 | F4BAA4402658297300DA5BF7 /* LaunchScreen.storyboard in Resources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | /* End PBXResourcesBuildPhase section */ 258 | 259 | /* Begin PBXSourcesBuildPhase section */ 260 | F43F827726578D83001D9B3D /* Sources */ = { 261 | isa = PBXSourcesBuildPhase; 262 | buildActionMask = 2147483647; 263 | files = ( 264 | F43F82A126578DCA001D9B3D /* PresetsController.swift in Sources */, 265 | F4B96F9C265AC36F00A01A29 /* IndicatorPresetModel.swift in Sources */, 266 | F43F829F26578DCA001D9B3D /* AppDelegate.swift in Sources */, 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | }; 270 | F43F82A726578F03001D9B3D /* Sources */ = { 271 | isa = PBXSourcesBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | F4BAA4422658297300DA5BF7 /* AppDelegate.swift in Sources */, 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | /* End PBXSourcesBuildPhase section */ 279 | 280 | /* Begin PBXVariantGroup section */ 281 | F43F829526578DCA001D9B3D /* LaunchScreen.storyboard */ = { 282 | isa = PBXVariantGroup; 283 | children = ( 284 | F43F829626578DCA001D9B3D /* Base */, 285 | ); 286 | name = LaunchScreen.storyboard; 287 | sourceTree = ""; 288 | }; 289 | /* End PBXVariantGroup section */ 290 | 291 | /* Begin XCBuildConfiguration section */ 292 | F43F828D26578D84001D9B3D /* Debug */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ALWAYS_SEARCH_USER_PATHS = NO; 296 | CLANG_ANALYZER_NONNULL = YES; 297 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 298 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 299 | CLANG_CXX_LIBRARY = "libc++"; 300 | CLANG_ENABLE_MODULES = YES; 301 | CLANG_ENABLE_OBJC_ARC = YES; 302 | CLANG_ENABLE_OBJC_WEAK = YES; 303 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 304 | CLANG_WARN_BOOL_CONVERSION = YES; 305 | CLANG_WARN_COMMA = YES; 306 | CLANG_WARN_CONSTANT_CONVERSION = YES; 307 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 308 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 309 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 310 | CLANG_WARN_EMPTY_BODY = YES; 311 | CLANG_WARN_ENUM_CONVERSION = YES; 312 | CLANG_WARN_INFINITE_RECURSION = YES; 313 | CLANG_WARN_INT_CONVERSION = YES; 314 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 315 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 316 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 317 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 318 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 319 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 320 | CLANG_WARN_STRICT_PROTOTYPES = YES; 321 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 322 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 323 | CLANG_WARN_UNREACHABLE_CODE = YES; 324 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 325 | COPY_PHASE_STRIP = NO; 326 | DEBUG_INFORMATION_FORMAT = dwarf; 327 | ENABLE_STRICT_OBJC_MSGSEND = YES; 328 | ENABLE_TESTABILITY = YES; 329 | GCC_C_LANGUAGE_STANDARD = gnu11; 330 | GCC_DYNAMIC_NO_PIC = NO; 331 | GCC_NO_COMMON_BLOCKS = YES; 332 | GCC_OPTIMIZATION_LEVEL = 0; 333 | GCC_PREPROCESSOR_DEFINITIONS = ( 334 | "DEBUG=1", 335 | "$(inherited)", 336 | ); 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 344 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 345 | MTL_FAST_MATH = YES; 346 | ONLY_ACTIVE_ARCH = YES; 347 | SDKROOT = iphoneos; 348 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 349 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 350 | }; 351 | name = Debug; 352 | }; 353 | F43F828E26578D84001D9B3D /* Release */ = { 354 | isa = XCBuildConfiguration; 355 | buildSettings = { 356 | ALWAYS_SEARCH_USER_PATHS = NO; 357 | CLANG_ANALYZER_NONNULL = YES; 358 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 359 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 360 | CLANG_CXX_LIBRARY = "libc++"; 361 | CLANG_ENABLE_MODULES = YES; 362 | CLANG_ENABLE_OBJC_ARC = YES; 363 | CLANG_ENABLE_OBJC_WEAK = YES; 364 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 365 | CLANG_WARN_BOOL_CONVERSION = YES; 366 | CLANG_WARN_COMMA = YES; 367 | CLANG_WARN_CONSTANT_CONVERSION = YES; 368 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 369 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 370 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 371 | CLANG_WARN_EMPTY_BODY = YES; 372 | CLANG_WARN_ENUM_CONVERSION = YES; 373 | CLANG_WARN_INFINITE_RECURSION = YES; 374 | CLANG_WARN_INT_CONVERSION = YES; 375 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 376 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 377 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 379 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 380 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 381 | CLANG_WARN_STRICT_PROTOTYPES = YES; 382 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 383 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 384 | CLANG_WARN_UNREACHABLE_CODE = YES; 385 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 386 | COPY_PHASE_STRIP = NO; 387 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 388 | ENABLE_NS_ASSERTIONS = NO; 389 | ENABLE_STRICT_OBJC_MSGSEND = YES; 390 | GCC_C_LANGUAGE_STANDARD = gnu11; 391 | GCC_NO_COMMON_BLOCKS = YES; 392 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 393 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 394 | GCC_WARN_UNDECLARED_SELECTOR = YES; 395 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 396 | GCC_WARN_UNUSED_FUNCTION = YES; 397 | GCC_WARN_UNUSED_VARIABLE = YES; 398 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 399 | MTL_ENABLE_DEBUG_INFO = NO; 400 | MTL_FAST_MATH = YES; 401 | SDKROOT = iphoneos; 402 | SWIFT_COMPILATION_MODE = wholemodule; 403 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 404 | VALIDATE_PRODUCT = YES; 405 | }; 406 | name = Release; 407 | }; 408 | F43F829026578D84001D9B3D /* Debug */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 412 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; 413 | CODE_SIGN_STYLE = Automatic; 414 | DEVELOPMENT_TEAM = NPYZ6A48AA; 415 | INFOPLIST_FILE = "iOS Example/Info.plist"; 416 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 417 | LD_RUNPATH_SEARCH_PATHS = ( 418 | "$(inherited)", 419 | "@executable_path/Frameworks", 420 | ); 421 | PRODUCT_BUNDLE_IDENTIFIER = by.ivanvorobei.opensource.spindicator.ios; 422 | PRODUCT_NAME = "$(TARGET_NAME)"; 423 | SWIFT_VERSION = 5.0; 424 | TARGETED_DEVICE_FAMILY = "1,2"; 425 | }; 426 | name = Debug; 427 | }; 428 | F43F829126578D84001D9B3D /* Release */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 432 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; 433 | CODE_SIGN_STYLE = Automatic; 434 | DEVELOPMENT_TEAM = NPYZ6A48AA; 435 | INFOPLIST_FILE = "iOS Example/Info.plist"; 436 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 437 | LD_RUNPATH_SEARCH_PATHS = ( 438 | "$(inherited)", 439 | "@executable_path/Frameworks", 440 | ); 441 | PRODUCT_BUNDLE_IDENTIFIER = by.ivanvorobei.opensource.spindicator.ios; 442 | PRODUCT_NAME = "$(TARGET_NAME)"; 443 | SWIFT_VERSION = 5.0; 444 | TARGETED_DEVICE_FAMILY = "1,2"; 445 | }; 446 | name = Release; 447 | }; 448 | F43F82BB26578F04001D9B3D /* Debug */ = { 449 | isa = XCBuildConfiguration; 450 | buildSettings = { 451 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 452 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; 453 | CODE_SIGN_STYLE = Automatic; 454 | DEVELOPMENT_TEAM = T9Z76HLJ3T; 455 | INFOPLIST_FILE = "tvOS Example/Info.plist"; 456 | LD_RUNPATH_SEARCH_PATHS = ( 457 | "$(inherited)", 458 | "@executable_path/Frameworks", 459 | ); 460 | PRODUCT_BUNDLE_IDENTIFIER = by.ivanvorobei.opensource.spindicator.tvos; 461 | PRODUCT_NAME = "$(TARGET_NAME)"; 462 | SDKROOT = appletvos; 463 | SWIFT_VERSION = 5.0; 464 | TARGETED_DEVICE_FAMILY = 3; 465 | TVOS_DEPLOYMENT_TARGET = 12.1; 466 | }; 467 | name = Debug; 468 | }; 469 | F43F82BC26578F04001D9B3D /* Release */ = { 470 | isa = XCBuildConfiguration; 471 | buildSettings = { 472 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 473 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; 474 | CODE_SIGN_STYLE = Automatic; 475 | DEVELOPMENT_TEAM = T9Z76HLJ3T; 476 | INFOPLIST_FILE = "tvOS Example/Info.plist"; 477 | LD_RUNPATH_SEARCH_PATHS = ( 478 | "$(inherited)", 479 | "@executable_path/Frameworks", 480 | ); 481 | PRODUCT_BUNDLE_IDENTIFIER = by.ivanvorobei.opensource.spindicator.tvos; 482 | PRODUCT_NAME = "$(TARGET_NAME)"; 483 | SDKROOT = appletvos; 484 | SWIFT_VERSION = 5.0; 485 | TARGETED_DEVICE_FAMILY = 3; 486 | TVOS_DEPLOYMENT_TARGET = 12.1; 487 | }; 488 | name = Release; 489 | }; 490 | /* End XCBuildConfiguration section */ 491 | 492 | /* Begin XCConfigurationList section */ 493 | F43F827626578D83001D9B3D /* Build configuration list for PBXProject "SPIndicator" */ = { 494 | isa = XCConfigurationList; 495 | buildConfigurations = ( 496 | F43F828D26578D84001D9B3D /* Debug */, 497 | F43F828E26578D84001D9B3D /* Release */, 498 | ); 499 | defaultConfigurationIsVisible = 0; 500 | defaultConfigurationName = Release; 501 | }; 502 | F43F828F26578D84001D9B3D /* Build configuration list for PBXNativeTarget "iOS Example" */ = { 503 | isa = XCConfigurationList; 504 | buildConfigurations = ( 505 | F43F829026578D84001D9B3D /* Debug */, 506 | F43F829126578D84001D9B3D /* Release */, 507 | ); 508 | defaultConfigurationIsVisible = 0; 509 | defaultConfigurationName = Release; 510 | }; 511 | F43F82BA26578F04001D9B3D /* Build configuration list for PBXNativeTarget "tvOS Example" */ = { 512 | isa = XCConfigurationList; 513 | buildConfigurations = ( 514 | F43F82BB26578F04001D9B3D /* Debug */, 515 | F43F82BC26578F04001D9B3D /* Release */, 516 | ); 517 | defaultConfigurationIsVisible = 0; 518 | defaultConfigurationName = Release; 519 | }; 520 | /* End XCConfigurationList section */ 521 | 522 | /* Begin XCSwiftPackageProductDependency section */ 523 | F457C6AA276927C1005B4E19 /* AppImport */ = { 524 | isa = XCSwiftPackageProductDependency; 525 | productName = AppImport; 526 | }; 527 | F457C6AC27692A1B005B4E19 /* AppImport */ = { 528 | isa = XCSwiftPackageProductDependency; 529 | productName = AppImport; 530 | }; 531 | /* End XCSwiftPackageProductDependency section */ 532 | }; 533 | rootObject = F43F827326578D83001D9B3D /* Project object */; 534 | } 535 | -------------------------------------------------------------------------------- /Sources/SPIndicator/SPIndicatorView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | #if os(iOS) 25 | 26 | /** 27 | SPIndicator: Main view. Can be customisable if need. 28 | 29 | For change duration, check method `present` and pass duration and other specific property if need customise. 30 | 31 | Here available set window on which shoud be present. 32 | If you have some windows, you shoud configure it. Check property `presentWindow`. 33 | 34 | For disable dismiss by drag, check property `.dismissByDrag`. 35 | 36 | Recomended call `SPIndicator` and choose style func. 37 | */ 38 | @available(iOSApplicationExtension, unavailable) 39 | open class SPIndicatorView: UIView { 40 | 41 | // MARK: - UIAppearance 42 | 43 | @objc dynamic open var duration: TimeInterval = 1.5 44 | 45 | // MARK: - Properties 46 | 47 | /** 48 | SPIndicator: Change it for set `top` or `bottom` present side. 49 | Shoud be change before present, instead of no effect. 50 | */ 51 | open var presentSide: SPIndicatorPresentSide = .top 52 | 53 | /** 54 | SPIndicator: By default allow drag indicator for hide. 55 | While indicator is dragging, dismiss not work. 56 | This behaviar can be disabled. 57 | */ 58 | open var dismissByDrag: Bool = true { 59 | didSet { 60 | setGesture() 61 | } 62 | } 63 | 64 | /** 65 | SPIndicator: Completion call after hide indicator. 66 | */ 67 | open var completion: (() -> Void)? = nil 68 | 69 | // MARK: - Views 70 | 71 | open var titleLabel: UILabel? 72 | open var subtitleLabel: UILabel? 73 | open var iconView: UIView? 74 | 75 | private lazy var backgroundView: UIVisualEffectView = { 76 | let view: UIVisualEffectView = { 77 | if #available(iOS 13.0, *) { 78 | return UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial)) 79 | } else { 80 | return UIVisualEffectView(effect: UIBlurEffect(style: .light)) 81 | } 82 | }() 83 | view.isUserInteractionEnabled = false 84 | return view 85 | }() 86 | 87 | weak open var presentWindow: UIWindow? 88 | 89 | // MARK: - Init 90 | 91 | public init(title: String, message: String? = nil, preset: SPIndicatorIconPreset) { 92 | super.init(frame: CGRect.zero) 93 | commonInit() 94 | layout = SPIndicatorLayout(for: preset) 95 | setTitle(title) 96 | if let message = message { 97 | setMessage(message) 98 | } 99 | setIcon(for: preset) 100 | } 101 | 102 | public init(title: String, message: String?) { 103 | super.init(frame: CGRect.zero) 104 | titleAreaFactor = 1.8 105 | minimumAreaWidth = 100 106 | commonInit() 107 | layout = SPIndicatorLayout.message() 108 | setTitle(title) 109 | if let message = message { 110 | setMessage(message) 111 | } 112 | } 113 | 114 | public required init?(coder aDecoder: NSCoder) { 115 | self.presentSide = .top 116 | super.init(coder: aDecoder) 117 | commonInit() 118 | } 119 | 120 | private func commonInit() { 121 | preservesSuperviewLayoutMargins = false 122 | if #available(iOS 11.0, *) { 123 | insetsLayoutMarginsFromSafeArea = false 124 | } 125 | 126 | backgroundColor = .clear 127 | backgroundView.layer.masksToBounds = true 128 | addSubview(backgroundView) 129 | 130 | setShadow() 131 | setGesture() 132 | } 133 | 134 | // MARK: - Configure 135 | 136 | private func setTitle(_ text: String) { 137 | let label = UILabel() 138 | label.font = UIFont.preferredFont(forTextStyle: .footnote, weight: .semibold, addPoints: 0) 139 | label.numberOfLines = 1 140 | let style = NSMutableParagraphStyle() 141 | style.lineBreakMode = .byTruncatingTail 142 | style.lineSpacing = 3 143 | label.attributedText = NSAttributedString( 144 | string: text, attributes: [.paragraphStyle: style] 145 | ) 146 | label.textAlignment = .center 147 | label.textColor = UIColor.Compability.label.withAlphaComponent(0.6) 148 | titleLabel = label 149 | addSubview(label) 150 | } 151 | 152 | private func setMessage(_ text: String) { 153 | let label = UILabel() 154 | label.font = UIFont.preferredFont(forTextStyle: .footnote, weight: .semibold, addPoints: 0) 155 | label.numberOfLines = 1 156 | let style = NSMutableParagraphStyle() 157 | style.lineBreakMode = .byTruncatingTail 158 | style.lineSpacing = 2 159 | label.attributedText = NSAttributedString( 160 | string: text, attributes: [.paragraphStyle: style] 161 | ) 162 | label.textAlignment = .center 163 | label.textColor = UIColor.Compability.label.withAlphaComponent(0.3) 164 | subtitleLabel = label 165 | addSubview(label) 166 | } 167 | 168 | private func setIcon(for preset: SPIndicatorIconPreset) { 169 | let view = preset.createView() 170 | self.iconView = view 171 | addSubview(view) 172 | } 173 | 174 | private func setShadow() { 175 | layer.shadowColor = UIColor.black.cgColor 176 | layer.shadowOpacity = 0.22 177 | layer.shadowOffset = .init(width: 0, height: 7) 178 | layer.shadowRadius = 40 179 | 180 | // Not use render shadow becouse backgorund is visual effect. 181 | // If turn on it, background will hide. 182 | // layer.shouldRasterize = true 183 | } 184 | 185 | private func setGesture() { 186 | if dismissByDrag { 187 | let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) 188 | addGestureRecognizer(gestureRecognizer) 189 | self.gestureRecognizer = gestureRecognizer 190 | } else { 191 | self.gestureRecognizer = nil 192 | } 193 | } 194 | 195 | // MARK: - Present 196 | 197 | private var presentAndDismissDuration: TimeInterval = 0.6 198 | 199 | private var presentWithOpacity: Bool { 200 | if presentSide == .center { return true } 201 | return false 202 | } 203 | 204 | open func present(haptic: SPIndicatorHaptic = .success, completion: (() -> Void)? = nil) { 205 | present(duration: self.duration, haptic: haptic, completion: completion) 206 | } 207 | 208 | open func present(duration: TimeInterval, haptic: SPIndicatorHaptic = .success, completion: (() -> Void)? = nil) { 209 | 210 | if self.presentWindow == nil { 211 | self.presentWindow = UIApplication.shared.keyWindow 212 | } 213 | 214 | guard let window = self.presentWindow else { return } 215 | 216 | window.addSubview(self) 217 | 218 | // Prepare for present 219 | 220 | self.whenGestureEndShoudHide = false 221 | self.completion = completion 222 | 223 | isHidden = true 224 | sizeToFit() 225 | layoutSubviews() 226 | center.x = window.frame.midX 227 | toPresentPosition(.prepare(presentSide)) 228 | 229 | self.alpha = presentWithOpacity ? 0 : 1 230 | 231 | // Present 232 | 233 | isHidden = false 234 | haptic.impact() 235 | UIView.animate(withDuration: presentAndDismissDuration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveEaseOut], animations: { 236 | self.toPresentPosition(.visible(self.presentSide)) 237 | if self.presentWithOpacity { self.alpha = 1 } 238 | }, completion: { finished in 239 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration) { 240 | if self.gestureIsDragging { 241 | self.whenGestureEndShoudHide = true 242 | } else { 243 | self.dismiss() 244 | } 245 | } 246 | }) 247 | 248 | if let iconView = self.iconView as? SPIndicatorIconAnimatable { 249 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + presentAndDismissDuration / 3) { 250 | iconView.animate() 251 | } 252 | } 253 | } 254 | 255 | @objc open func dismiss() { 256 | UIView.animate(withDuration: presentAndDismissDuration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: { 257 | self.toPresentPosition(.prepare(self.presentSide)) 258 | if self.presentWithOpacity { self.alpha = 0 } 259 | }, completion: { finished in 260 | self.removeFromSuperview() 261 | self.completion?() 262 | }) 263 | } 264 | 265 | // MARK: - Internal 266 | 267 | private var minimumYTranslationForHideByGesture: CGFloat = -10 268 | private var maximumYTranslationByGesture: CGFloat = 60 269 | 270 | private var gestureRecognizer: UIPanGestureRecognizer? 271 | private var gestureIsDragging: Bool = false 272 | private var whenGestureEndShoudHide: Bool = false 273 | 274 | @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { 275 | 276 | if gestureRecognizer.state == .began || gestureRecognizer.state == .changed { 277 | self.gestureIsDragging = true 278 | let translation = gestureRecognizer.translation(in: self) 279 | let newTranslation: CGFloat = { 280 | switch presentSide { 281 | case .top: 282 | if translation.y <= 0 { 283 | return translation.y 284 | } else { 285 | return min(maximumYTranslationByGesture, translation.y.squareRoot()) 286 | } 287 | case .bottom: 288 | if translation.y >= 0 { 289 | return translation.y 290 | } else { 291 | let absolute = abs(translation.y) 292 | return -min(maximumYTranslationByGesture, absolute.squareRoot()) 293 | } 294 | case .center: 295 | let absolute = abs(translation.y).squareRoot() 296 | let newValue = translation.y < 0 ? -absolute : absolute 297 | return min(maximumYTranslationByGesture, newValue) 298 | } 299 | }() 300 | toPresentPosition(.fromVisible(newTranslation, from: (presentSide))) 301 | } 302 | 303 | if gestureRecognizer.state == .ended { 304 | gestureIsDragging = false 305 | 306 | var shoudDismissWhenEndAnimation: Bool = false 307 | 308 | UIView.animate(withDuration: presentAndDismissDuration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: { 309 | if self.whenGestureEndShoudHide { 310 | self.toPresentPosition(.prepare(self.presentSide)) 311 | shoudDismissWhenEndAnimation = true 312 | } else { 313 | let translation = gestureRecognizer.translation(in: self) 314 | if translation.y < self.minimumYTranslationForHideByGesture { 315 | self.toPresentPosition(.prepare(self.presentSide)) 316 | shoudDismissWhenEndAnimation = true 317 | } else { 318 | self.toPresentPosition(.visible(self.presentSide)) 319 | } 320 | } 321 | }, completion: { _ in 322 | if shoudDismissWhenEndAnimation { 323 | self.dismiss() 324 | } 325 | }) 326 | } 327 | } 328 | 329 | private func toPresentPosition(_ position: PresentPosition) { 330 | 331 | let getPrepareTransform: ((_ side: SPIndicatorPresentSide) -> CGAffineTransform) = { [weak self] side in 332 | guard let self = self else { return .identity } 333 | guard let window = UIApplication.shared.windows.first else { return .identity } 334 | switch side { 335 | case .top: 336 | let topInset = window.safeAreaInsets.top 337 | let position = -(topInset + 50) 338 | return CGAffineTransform.identity.translatedBy(x: 0, y: position) 339 | case .bottom: 340 | let height = window.frame.height 341 | let bottomInset = window.safeAreaInsets.bottom 342 | let position = height + bottomInset + 50 343 | return CGAffineTransform.identity.translatedBy(x: 0, y: position) 344 | case .center: 345 | return CGAffineTransform.identity.translatedBy(x: 0, y: window.frame.height / 2 - self.frame.height / 2).scaledBy(x: 0.9, y: 0.9) 346 | } 347 | } 348 | 349 | let getVisibleTransform: ((_ side: SPIndicatorPresentSide) -> CGAffineTransform) = { [weak self] side in 350 | guard let self = self else { return .identity } 351 | guard let window = UIApplication.shared.windows.first else { return .identity } 352 | switch side { 353 | case .top: 354 | var topSafeAreaInsets = window.safeAreaInsets.top 355 | if topSafeAreaInsets < 20 { topSafeAreaInsets = 20 } 356 | let position = topSafeAreaInsets - 3 + self.offset 357 | return CGAffineTransform.identity.translatedBy(x: 0, y: position) 358 | case .bottom: 359 | let height = window.frame.height 360 | var bottomSafeAreaInsets = window.safeAreaInsets.top 361 | if bottomSafeAreaInsets < 20 { bottomSafeAreaInsets = 20 } 362 | let position = height - bottomSafeAreaInsets - 3 - self.frame.height - self.offset 363 | return CGAffineTransform.identity.translatedBy(x: 0, y: position) 364 | case .center: 365 | return CGAffineTransform.identity.translatedBy(x: 0, y: window.frame.height / 2 - self.frame.height / 2) 366 | } 367 | } 368 | 369 | switch position { 370 | case .prepare(let presentSide): 371 | transform = getPrepareTransform(presentSide) 372 | case .visible(let presentSide): 373 | transform = getVisibleTransform(presentSide) 374 | case .fromVisible(let translation, let presentSide): 375 | transform = getVisibleTransform(presentSide).translatedBy(x: 0, y: translation) 376 | } 377 | } 378 | 379 | // MARK: - Layout 380 | 381 | /** 382 | SPIndicator: Wraper of layout values. 383 | */ 384 | open var layout: SPIndicatorLayout = .init() 385 | 386 | /** 387 | SPIndicator: Alert offset 388 | */ 389 | open var offset: CGFloat = 0 390 | 391 | private var areaHeight: CGFloat = 50 392 | private var minimumAreaWidth: CGFloat = 196 393 | private var maximumAreaWidth: CGFloat = 260 394 | private var titleAreaFactor: CGFloat = 2.5 395 | private var spaceBetweenTitles: CGFloat = 1 396 | private var spaceBetweenTitlesAndImage: CGFloat = 16 397 | 398 | private var titlesCompactWidth: CGFloat { 399 | if let iconView = self.iconView { 400 | let space = iconView.frame.maxY + spaceBetweenTitlesAndImage 401 | return frame.width - space * 2 402 | } else { 403 | return frame.width - layoutMargins.left - layoutMargins.right 404 | } 405 | } 406 | 407 | private var titlesFullWidth: CGFloat { 408 | if let iconView = self.iconView { 409 | let space = iconView.frame.maxY + spaceBetweenTitlesAndImage 410 | return frame.width - space - layoutMargins.right - self.spaceBetweenTitlesAndImage 411 | } else { 412 | return frame.width - layoutMargins.left - layoutMargins.right 413 | } 414 | } 415 | 416 | open override func sizeThatFits(_ size: CGSize) -> CGSize { 417 | titleLabel?.sizeToFit() 418 | let titleWidth: CGFloat = titleLabel?.frame.width ?? 0 419 | subtitleLabel?.sizeToFit() 420 | let subtitleWidth: CGFloat = subtitleLabel?.frame.width ?? 0 421 | var width = (max(titleWidth, subtitleWidth) * titleAreaFactor).rounded() 422 | 423 | if width < minimumAreaWidth { width = minimumAreaWidth } 424 | if width > maximumAreaWidth { width = maximumAreaWidth } 425 | 426 | return .init(width: width, height: areaHeight) 427 | } 428 | 429 | open override func layoutSubviews() { 430 | super.layoutSubviews() 431 | 432 | layoutMargins = layout.margins 433 | layer.cornerRadius = frame.height / 2 434 | backgroundView.frame = bounds 435 | backgroundView.layer.cornerRadius = layer.cornerRadius 436 | 437 | // Flags 438 | 439 | let hasIcon = (self.iconView != nil) 440 | let hasTitle = (self.titleLabel != nil) 441 | let hasSubtite = (self.subtitleLabel != nil) 442 | 443 | let fitTitleToCompact: Bool = { 444 | guard let titleLabel = self.titleLabel else { return true } 445 | titleLabel.numberOfLines = 1 446 | titleLabel.sizeToFit() 447 | return titleLabel.frame.width < titlesCompactWidth 448 | }() 449 | 450 | let fitSubtitleToCompact: Bool = { 451 | guard let subtitleLabel = self.subtitleLabel else { return true } 452 | subtitleLabel.numberOfLines = 1 453 | subtitleLabel.sizeToFit() 454 | return subtitleLabel.frame.width < titlesCompactWidth 455 | }() 456 | 457 | let notFitAnyLabelToCompact: Bool = { 458 | if !fitTitleToCompact { return true } 459 | if !fitSubtitleToCompact { return true } 460 | return false 461 | }() 462 | 463 | var layout: LayoutGrid = .iconTitleCentered 464 | 465 | if (hasIcon && hasTitle && hasSubtite) && !notFitAnyLabelToCompact { 466 | layout = .iconTitleMessageCentered 467 | } 468 | 469 | if (hasIcon && hasTitle && hasSubtite) && notFitAnyLabelToCompact { 470 | layout = .iconTitleMessageLeading 471 | } 472 | 473 | if (hasIcon && hasTitle && !hasSubtite) { 474 | layout = .iconTitleCentered 475 | } 476 | 477 | if (!hasIcon && hasTitle && !hasSubtite) { 478 | layout = .title 479 | } 480 | 481 | if (!hasIcon && hasTitle && hasSubtite) { 482 | layout = .titleMessage 483 | } 484 | 485 | // Actions 486 | 487 | let layoutIcon = { [weak self] in 488 | guard let self = self else { return } 489 | guard let iconView = self.iconView else { return } 490 | iconView.frame = .init( 491 | origin: .init(x: self.layoutMargins.left, y: iconView.frame.origin.y), 492 | size: self.layout.iconSize 493 | ) 494 | iconView.center.y = self.bounds.midY 495 | } 496 | 497 | let layoutTitleCenteredCompact = { [weak self] in 498 | guard let self = self else { return } 499 | guard let titleLabel = self.titleLabel else { return } 500 | titleLabel.textAlignment = .center 501 | titleLabel.layoutDynamicHeight(width: self.titlesCompactWidth) 502 | titleLabel.center.x = self.frame.width / 2 503 | } 504 | 505 | let layoutTitleCenteredFullWidth = { [weak self] in 506 | guard let self = self else { return } 507 | guard let titleLabel = self.titleLabel else { return } 508 | titleLabel.textAlignment = .center 509 | titleLabel.layoutDynamicHeight(width: self.titlesFullWidth) 510 | titleLabel.center.x = self.frame.width / 2 511 | } 512 | 513 | let layoutTitleLeadingFullWidth = { [weak self] in 514 | guard let self = self else { return } 515 | guard let titleLabel = self.titleLabel else { return } 516 | guard let iconView = self.iconView else { return } 517 | let rtl = self.effectiveUserInterfaceLayoutDirection == .rightToLeft 518 | titleLabel.textAlignment = rtl ? .right : .left 519 | titleLabel.layoutDynamicHeight(width: self.titlesFullWidth) 520 | titleLabel.frame.origin.x = self.layoutMargins.left + iconView.frame.width + self.spaceBetweenTitlesAndImage 521 | } 522 | 523 | let layoutSubtitle = { [weak self] in 524 | guard let self = self else { return } 525 | guard let titleLabel = self.titleLabel else { return } 526 | guard let subtitleLabel = self.subtitleLabel else { return } 527 | subtitleLabel.textAlignment = titleLabel.textAlignment 528 | subtitleLabel.layoutDynamicHeight(width: titleLabel.frame.width) 529 | subtitleLabel.frame.origin.x = titleLabel.frame.origin.x 530 | } 531 | 532 | let layoutTitleSubtitleByVertical = { [weak self] in 533 | guard let self = self else { return } 534 | guard let titleLabel = self.titleLabel else { return } 535 | guard let subtitleLabel = self.subtitleLabel else { 536 | titleLabel.center.y = self.bounds.midY 537 | return 538 | } 539 | let allHeight = titleLabel.frame.height + subtitleLabel.frame.height + self.spaceBetweenTitles 540 | titleLabel.frame.origin.y = (self.frame.height - allHeight) / 2 541 | subtitleLabel.frame.origin.y = titleLabel.frame.maxY + self.spaceBetweenTitles 542 | } 543 | 544 | // Apply 545 | 546 | switch layout { 547 | case .iconTitleMessageCentered: 548 | layoutIcon() 549 | layoutTitleCenteredCompact() 550 | layoutSubtitle() 551 | case .iconTitleMessageLeading: 552 | layoutIcon() 553 | layoutTitleLeadingFullWidth() 554 | layoutSubtitle() 555 | case .iconTitleCentered: 556 | layoutIcon() 557 | titleLabel?.numberOfLines = 2 558 | layoutTitleCenteredCompact() 559 | case .iconTitleLeading: 560 | layoutIcon() 561 | titleLabel?.numberOfLines = 2 562 | layoutTitleLeadingFullWidth() 563 | case .title: 564 | titleLabel?.numberOfLines = 2 565 | layoutTitleCenteredFullWidth() 566 | case .titleMessage: 567 | layoutTitleCenteredFullWidth() 568 | layoutSubtitle() 569 | } 570 | 571 | layoutTitleSubtitleByVertical() 572 | } 573 | 574 | // MARK: - Models 575 | 576 | enum PresentPosition { 577 | 578 | case prepare(_ from: SPIndicatorPresentSide) 579 | case visible(_ from: SPIndicatorPresentSide) 580 | case fromVisible(_ translation: CGFloat, from: SPIndicatorPresentSide) 581 | } 582 | 583 | enum LayoutGrid { 584 | 585 | case iconTitleMessageCentered 586 | case iconTitleMessageLeading 587 | case iconTitleCentered 588 | case iconTitleLeading 589 | case title 590 | case titleMessage 591 | } 592 | } 593 | 594 | #endif 595 | --------------------------------------------------------------------------------