├── .github
    ├── FUNDING.yml
    ├── ISSUE_TEMPLATE
    │   ├── bug_report.md
    │   └── feature_request.md
    └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Package.swift
├── README.md
├── SPAlert.podspec
└── Sources
    └── AlertKit
        ├── AlertHaptic.swift
        ├── AlertIcon.swift
        ├── AlertKitAPI.swift
        ├── AlertViewStyle.swift
        ├── Extensions
            ├── SwiftUIExtension.swift
            ├── UIFontExtension.swift
            └── UILabelExtension.swift
        ├── Icons
            ├── AlertIconDoneView.swift
            ├── AlertIconErrorView.swift
            ├── AlertIconHeartView.swift
            └── AlertSpinnerView.swift
        └── Views
            ├── AlertAppleMusic16View.swift
            ├── AlertAppleMusic17View.swift
            ├── AlertViewInternalDismissProtocol.swift
            └── AlertViewProtocol.swift
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [sparrowcode]
2 | 
--------------------------------------------------------------------------------
/.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 | 
--------------------------------------------------------------------------------
/.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 | 
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Goal
2 | 
3 | 
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS Files
2 | .DS_Store
3 | .Trashes
4 | 
5 | # Swift Package Manager
6 | .swiftpm
7 | 
--------------------------------------------------------------------------------
/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 | 
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # Contributing
 2 | 
 3 | Here provided info about contribution process and recommendations.
 4 | 
 5 | ## Codestyle 
 6 | 
 7 | ### Marks
 8 | 
 9 | For clean struct code good is using marks. 
10 | 
11 | ```swift
12 | class Example {
13 | 
14 |     // MARK: - Init
15 |     
16 |     init() {}
17 | }
18 | ```
19 | 
20 | Here you find all which using in project:
21 | 
22 | - // MARK: - Init
23 | - // MARK: - Lifecycle
24 | - // MARK: - Layout
25 | - // MARK: - Public
26 | - // MARK: - Private
27 | - // MARK: - Internal
28 | - // MARK: - Models
29 | - // MARK: - Ovveride
30 | 
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright © 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 | 
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
 1 | // swift-tools-version:5.9
 2 | 
 3 | import PackageDescription
 4 | 
 5 | let package = Package(
 6 |     name: "AlertKit",
 7 |     platforms: [
 8 |         .iOS(.v13),
 9 |         .visionOS(.v1)
10 |     ],
11 |     products: [
12 |         .library(
13 |             name: "AlertKit",
14 |             targets: ["AlertKit"]
15 |         )
16 |     ],
17 |     dependencies: [],
18 |     targets: [
19 |         .target(
20 |             name: "AlertKit",
21 |             swiftSettings: [
22 |                 .define("ALERTKIT_SPM")
23 |             ]
24 |         )
25 |     ],
26 |     swiftLanguageVersions: [.v5]
27 | )
28 | 
29 | 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # AlertKit
  2 | 
  3 | **Popup from Apple Music & Feedback in AppStore**. Contains `Done`, `Heart`, `Error` and other. Supports Dark Mode.
  4 | I tried to recreate Apple's alerts as much as possible. You can find these alerts in the AppStore after feedback and after you add a song to your library in Apple Music.
  5 | 
  6 | 
  7 | 
  8 | For UIKit & SwiftUI call this:
  9 | 
 10 | ```swift
 11 | AlertKitAPI.present(
 12 |     title: "Added to Library",
 13 |     icon: .done,
 14 |     style: .iOS17AppleMusic,
 15 |     haptic: .success
 16 | )
 17 | ```
 18 | 
 19 | Available 2 styles:
 20 | 
 21 | ```swift
 22 | public enum AlertViewStyle {
 23 | 
 24 |     case iOS16AppleMusic
 25 |     case iOS17AppleMusic
 26 | }
 27 | ```
 28 | 
 29 | ### iOS Dev Community
 30 | 
 31 | 
 32 |     
 33 |          34 |     
 35 |     
 36 |
 34 |     
 35 |     
 36 |          37 |     
 38 |
 37 |     
 38 | 
 39 | 
 40 | ## Navigate
 41 | 
 42 | - [Installation](#installation)
 43 |   - [Swift Package Manager](#swift-package-manager)
 44 |   - [CocoaPods](#cocoapods)
 45 | - [SwiftUI](#swiftui)
 46 | - [Present & Dismiss](#present--dismiss)
 47 | - [Customisation](#customisation)
 48 | - [Apps Using](#apps-using)
 49 | 
 50 | ## Installation
 51 | 
 52 | Ready to use on iOS 13+. Supports iOS and visionOS. Working with `UIKit` and `SwiftUI`.
 53 | 
 54 | ### Swift Package Manager
 55 | 
 56 | In Xcode go to Project -> Your Project Name -> `Package Dependencies` -> Tap _Plus_. Insert url:
 57 | 
 58 | ```
 59 | https://github.com/sparrowcode/AlertKit
 60 | ```
 61 | 
 62 | or adding it to the `dependencies` of your `Package.swift`:
 63 | 
 64 | ```swift
 65 | dependencies: [
 66 |     .package(url: "https://github.com/sparrowcode/AlertKit", .upToNextMajor(from: "5.1.8"))
 67 | ]
 68 | ```
 69 | 
 70 | ### CocoaPods:
 71 | 
 72 | This is an outdated way of doing things. I advise you to use [SPM](#swift-package-manager). However, I will continue to support Cocoapods for some time.
 73 | 
 74 | Cocoapods Installation
 75 | 
 76 | [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`:
 77 | 
 78 | ```ruby
 79 | pod 'SPAlert'
 80 | ```
 81 | 
 82 | 
134 |      135 |
135 |      136 |
136 |      137 |
137 |      138 |
138 |      139 |
139 |      140 |
140 |      141 |
141 |      142 |
142 |      143 |
143 | 
144 | 
145 | If you use a `AlertKit`, add your app via Pull Request.
146 | 
--------------------------------------------------------------------------------
/SPAlert.podspec:
--------------------------------------------------------------------------------
 1 | Pod::Spec.new do |s|
 2 | 
 3 |   s.name = 'SPAlert'
 4 |   s.version = '5.1.8'
 5 |   s.summary = 'Native alert from Apple Music & Feedback. Contains Done, Heart & Message and other presets. Support SwiftUI.'
 6 |   s.homepage = 'https://github.com/sparrowcode/AlertKit'
 7 |   s.source = { :git => 'https://github.com/sparrowcode/AlertKit.git', :tag => s.version }
 8 |   s.license = { :type => "MIT", :file => "LICENSE" }
 9 |   s.author = { 'Sparrow Code' => 'hello@sparrowcode.io' }
10 |   
11 |   s.swift_version = '5.1'
12 |   s.ios.deployment_target = '13.0'
13 |   #s.tvos.deployment_target = '13.0'
14 | 
15 |   s.source_files = 'Sources/AlertKit/**/*.swift'
16 | 
17 | end
18 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/AlertHaptic.swift:
--------------------------------------------------------------------------------
 1 | import UIKit
 2 | 
 3 | public enum AlertHaptic {
 4 |     
 5 |     case success
 6 |     case warning
 7 |     case error
 8 |     case none
 9 |     
10 |     func impact() {
11 |         #if os(iOS)
12 |         let generator = UINotificationFeedbackGenerator()
13 |         switch self {
14 |         case .success:
15 |             generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.success)
16 |         case .warning:
17 |             generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.warning)
18 |         case .error:
19 |             generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.error)
20 |         case .none:
21 |             break
22 |         }
23 |         #endif
24 |     }
25 | }
26 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/AlertIcon.swift:
--------------------------------------------------------------------------------
 1 | import UIKit
 2 | 
 3 | public enum AlertIcon: Equatable {
 4 |     
 5 |     case done
 6 |     case error
 7 |     case heart
 8 |     case spinnerSmall
 9 |     case spinnerLarge
10 |     
11 |     case custom(_ image: UIImage)
12 |     
13 |     func createView(lineThick: CGFloat) -> UIView {
14 |         switch self {
15 |         case .done: return AlertIconDoneView(lineThick: lineThick)
16 |         case .error: return AlertIconErrorView(lineThick: lineThick)
17 |         case .heart: return AlertIconHeartView()
18 |         case .spinnerSmall: return AlertSpinnerView(style: .medium)
19 |         case .spinnerLarge: return AlertSpinnerView(style: .large)
20 |         case .custom(let image):
21 |             let imageView = UIImageView(image: image)
22 |             imageView.contentMode = .scaleAspectFit
23 |             return imageView
24 |         }
25 |     }
26 | }
27 | 
28 | public protocol AlertIconAnimatable {
29 | 
30 |     func animate()
31 | }
32 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/AlertKitAPI.swift:
--------------------------------------------------------------------------------
 1 | import UIKit
 2 | 
 3 | public enum AlertKitAPI {
 4 |     
 5 |     public static func present(view: AlertViewProtocol, completion: @escaping ()->Void = {}) {
 6 |         guard let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first else { return }
 7 |         view.present(on: window, completion: completion)
 8 |     }
 9 |     
10 |     public static func present(title: String? = nil, subtitle: String? = nil, icon: AlertIcon? = nil, style: AlertViewStyle, haptic: AlertHaptic? = nil) {
11 |         switch style {
12 |         #if os(iOS)
13 |         case .iOS16AppleMusic:
14 |             guard let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first else { return }
15 |             let view = AlertAppleMusic16View(title: title, subtitle: subtitle, icon: icon)
16 |             view.haptic = haptic
17 |             view.present(on: window)
18 |         #endif
19 |         #if os(iOS) || os(visionOS)
20 |         case .iOS17AppleMusic:
21 |             guard let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first else { return }
22 |             let view = AlertAppleMusic17View(title: title, subtitle: subtitle, icon: icon)
23 |             view.haptic = haptic
24 |             view.present(on: window)
25 |         #endif
26 |         }
27 |     }
28 |     
29 |     /**
30 |      Call only with this one `completion`. Internal ones is canceled.
31 |      */
32 |     public static func dismissAllAlerts(completion: (() -> Void)? = nil) {
33 |         
34 |         var alertViews: [AlertViewInternalDismissProtocol] = []
35 |         
36 |         for window in UIApplication.shared.windows {
37 |             for view in window.subviews {
38 |                 if let view = view as? AlertViewInternalDismissProtocol {
39 |                     alertViews.append(view)
40 |                 }
41 |             }
42 |         }
43 |         
44 |         if alertViews.isEmpty {
45 |             completion?()
46 |         } else {
47 |             for (index, view) in alertViews.enumerated() {
48 |                 if index == .zero {
49 |                     view.dismiss(customCompletion: {
50 |                         completion?()
51 |                     })
52 |                 } else {
53 |                     view.dismiss(customCompletion: nil)
54 |                 }
55 |             }
56 |         }
57 |     }
58 | }
59 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/AlertViewStyle.swift:
--------------------------------------------------------------------------------
 1 | import Foundation
 2 | 
 3 | public enum AlertViewStyle {
 4 |     
 5 |     #if os(iOS)
 6 |     case iOS16AppleMusic
 7 |     #endif
 8 |     
 9 |     #if os(iOS) || os(visionOS)
10 |     case iOS17AppleMusic
11 |     #endif
12 | }
13 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Extensions/SwiftUIExtension.swift:
--------------------------------------------------------------------------------
 1 | import SwiftUI
 2 | 
 3 | @available(iOS 13.0, *)
 4 | extension View {
 5 |     
 6 |     public func alert(isPresent: Binding, view: AlertViewProtocol, completion: (()->Void)? = nil) -> some View {
 7 |         if isPresent.wrappedValue {
 8 |             let wrapperCompletion = {
 9 |                 isPresent.wrappedValue = false
10 |                 completion?()
11 |             }
12 |             if let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first {
13 |                 view.present(on: window, completion: wrapperCompletion)
14 |             }
15 |         }
16 |         return self
17 |     }
18 | }
19 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Extensions/UIFontExtension.swift:
--------------------------------------------------------------------------------
 1 | import UIKit
 2 | 
 3 | extension UIFont {
 4 |     
 5 |     static func preferredFont(forTextStyle style: TextStyle, weight: Weight, addPoints: CGFloat = 0) -> UIFont {
 6 |         let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style)
 7 |         let font = UIFont.systemFont(ofSize: descriptor.pointSize + addPoints, weight: weight)
 8 |         let metrics = UIFontMetrics(forTextStyle: style)
 9 |         return metrics.scaledFont(for: font)
10 |     }
11 | }
12 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Extensions/UILabelExtension.swift:
--------------------------------------------------------------------------------
 1 | import UIKit
 2 | 
 3 | extension UILabel {
 4 |     
 5 |     func layoutDynamicHeight(x: CGFloat, y: CGFloat, width: CGFloat) {
 6 |         frame = CGRect.init(x: x, y: y, width: width, height: frame.height)
 7 |         sizeToFit()
 8 |         if frame.width != width {
 9 |             frame = .init(x: x, y: y, width: width, height: frame.height)
10 |         }
11 |     }
12 | }
13 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Icons/AlertIconDoneView.swift:
--------------------------------------------------------------------------------
 1 | import UIKit
 2 | 
 3 | public class AlertIconDoneView: UIView, AlertIconAnimatable {
 4 |     
 5 |     private let lineThick: CGFloat
 6 |     
 7 |     init(lineThick: CGFloat) {
 8 |         self.lineThick = lineThick
 9 |         super.init(frame: .zero)
10 |     }
11 |     
12 |     required init?(coder: NSCoder) {
13 |         fatalError("init(coder:) has not been implemented")
14 |     }
15 |     
16 |     public func animate() {
17 |         let length = frame.width
18 |         let animatablePath = UIBezierPath()
19 |         animatablePath.move(to: CGPoint(x: length * 0.196, y: length * 0.527))
20 |         animatablePath.addLine(to: CGPoint(x: length * 0.47, y: length * 0.777))
21 |         animatablePath.addLine(to: CGPoint(x: length * 0.99, y: length * 0.25))
22 |         
23 |         let animatableLayer = CAShapeLayer()
24 |         animatableLayer.path = animatablePath.cgPath
25 |         animatableLayer.fillColor = UIColor.clear.cgColor
26 |         animatableLayer.strokeColor = tintColor?.cgColor
27 |         animatableLayer.lineWidth = lineThick
28 |         animatableLayer.lineCap = .round
29 |         animatableLayer.lineJoin = .round
30 |         animatableLayer.strokeEnd = 0
31 |         layer.addSublayer(animatableLayer)
32 |         
33 |         let animation = CABasicAnimation(keyPath: "strokeEnd")
34 |         animation.duration = 0.3
35 |         animation.fromValue = 0
36 |         animation.toValue = 1
37 |         animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
38 |         animatableLayer.strokeEnd = 1
39 |         animatableLayer.add(animation, forKey: "animation")
40 |     }
41 | }
42 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Icons/AlertIconErrorView.swift:
--------------------------------------------------------------------------------
 1 | import UIKit
 2 | 
 3 | public class AlertIconErrorView: UIView, AlertIconAnimatable {
 4 | 
 5 |     private let lineThick: CGFloat
 6 |     
 7 |     init(lineThick: CGFloat) {
 8 |         self.lineThick = lineThick
 9 |         super.init(frame: .zero)
10 |     }
11 |     
12 |     required init?(coder: NSCoder) {
13 |         fatalError("init(coder:) has not been implemented")
14 |     }
15 |     
16 |     public func animate() {
17 |         animateTopToBottomLine()
18 |         animateBottomToTopLine()
19 |     }
20 |         
21 |     private func animateTopToBottomLine() {
22 |         let length = frame.width
23 |         
24 |         let topToBottomLine = UIBezierPath()
25 |         topToBottomLine.move(to: CGPoint(x: length * 0, y: length * 0))
26 |         topToBottomLine.addLine(to: CGPoint(x: length * 1, y: length * 1))
27 |         
28 |         let animatableLayer = CAShapeLayer()
29 |         animatableLayer.path = topToBottomLine.cgPath
30 |         animatableLayer.fillColor = UIColor.clear.cgColor
31 |         animatableLayer.strokeColor = tintColor?.cgColor
32 |         animatableLayer.lineWidth = lineThick
33 |         animatableLayer.lineCap = .round
34 |         animatableLayer.lineJoin = .round
35 |         animatableLayer.strokeEnd = 0
36 |         self.layer.addSublayer(animatableLayer)
37 |         
38 |         let animation = CABasicAnimation(keyPath: "strokeEnd")
39 |         animation.duration = 0.22
40 |         animation.fromValue = 0
41 |         animation.toValue = 1
42 |         animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
43 |         
44 |         animatableLayer.strokeEnd = 1
45 |         animatableLayer.add(animation, forKey: "animation")
46 |     }
47 |         
48 |     private func animateBottomToTopLine() {
49 |         let length = frame.width
50 |         
51 |         let bottomToTopLine = UIBezierPath()
52 |         bottomToTopLine.move(to: CGPoint(x: length * 0, y: length * 1))
53 |         bottomToTopLine.addLine(to: CGPoint(x: length * 1, y: length * 0))
54 |         
55 |         let animatableLayer = CAShapeLayer()
56 |         animatableLayer.path = bottomToTopLine.cgPath
57 |         animatableLayer.fillColor = UIColor.clear.cgColor
58 |         animatableLayer.strokeColor = tintColor?.cgColor
59 |         animatableLayer.lineWidth = lineThick
60 |         animatableLayer.lineCap = .round
61 |         animatableLayer.lineJoin = .round
62 |         animatableLayer.strokeEnd = 0
63 |         self.layer.addSublayer(animatableLayer)
64 |         
65 |         let animation = CABasicAnimation(keyPath: "strokeEnd")
66 |         animation.duration = 0.22
67 |         animation.fromValue = 0
68 |         animation.toValue = 1
69 |         animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
70 |         
71 |         animatableLayer.strokeEnd = 1
72 |         animatableLayer.add(animation, forKey: "animation")
73 |     }
74 | }
75 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Icons/AlertIconHeartView.swift:
--------------------------------------------------------------------------------
 1 | import UIKit
 2 | 
 3 | public class AlertIconHeartView: UIView {
 4 |         
 5 |     init() {
 6 |         super.init(frame: .zero)
 7 |         self.backgroundColor = .clear
 8 |     }
 9 |     
10 |     required init?(coder aDecoder: NSCoder) {
11 |         fatalError("init(coder:) has not been implemented")
12 |     }
13 |     
14 |     public override func draw(_ rect: CGRect) {
15 |         super.draw(rect)
16 |         HeartDraw.draw(frame: rect, resizing: .aspectFit, fillColor: self.tintColor)
17 |     }
18 |     
19 |     class HeartDraw: NSObject {
20 |         
21 |         @objc dynamic public class func draw(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 510, height: 470), resizing: ResizingBehavior = .aspectFit, fillColor: UIColor = UIColor(red: 0.000, green: 0.000, blue: 0.000, alpha: 1.000)) {
22 |             let context = UIGraphicsGetCurrentContext()!
23 |             context.saveGState()
24 |             let resizedFrame: CGRect = resizing.apply(rect: CGRect(x: 0, y: 0, width: 510, height: 470), target: targetFrame)
25 |             context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
26 |             context.scaleBy(x: resizedFrame.width / 510, y: resizedFrame.height / 470)
27 |             let bezierPath = UIBezierPath()
28 |             bezierPath.move(to: CGPoint(x: 255, y: 469.6))
29 |             bezierPath.addLine(to: CGPoint(x: 219.3, y: 433.9))
30 |             bezierPath.addCurve(to: CGPoint(x: 0, y: 140.65), controlPoint1: CGPoint(x: 86.7, y: 316.6), controlPoint2: CGPoint(x: 0, y: 237.55))
31 |             bezierPath.addCurve(to: CGPoint(x: 140.25, y: 0.4), controlPoint1: CGPoint(x: 0, y: 61.6), controlPoint2: CGPoint(x: 61.2, y: 0.4))
32 |             bezierPath.addCurve(to: CGPoint(x: 255, y: 53.95), controlPoint1: CGPoint(x: 183.6, y: 0.4), controlPoint2: CGPoint(x: 226.95, y: 20.8))
33 |             bezierPath.addCurve(to: CGPoint(x: 369.75, y: 0.4), controlPoint1: CGPoint(x: 283.05, y: 20.8), controlPoint2: CGPoint(x: 326.4, y: 0.4))
34 |             bezierPath.addCurve(to: CGPoint(x: 510, y: 140.65), controlPoint1: CGPoint(x: 448.8, y: 0.4), controlPoint2: CGPoint(x: 510, y: 61.6))
35 |             bezierPath.addCurve(to: CGPoint(x: 290.7, y: 433.9), controlPoint1: CGPoint(x: 510, y: 237.55), controlPoint2: CGPoint(x: 423.3, y: 316.6))
36 |             bezierPath.addLine(to: CGPoint(x: 255, y: 469.6))
37 |             bezierPath.close()
38 |             fillColor.setFill()
39 |             bezierPath.fill()
40 |             context.restoreGState()
41 |         }
42 |         
43 |         @objc(HeartStyleKitResizingBehavior)
44 |         public enum ResizingBehavior: Int {
45 |             
46 |             case aspectFit
47 |             case aspectFill
48 |             case stretch
49 |             case center
50 |             
51 |             public func apply(rect: CGRect, target: CGRect) -> CGRect {
52 |                 if rect == target || target == CGRect.zero {
53 |                     return rect
54 |                 }
55 |                 
56 |                 var scales = CGSize.zero
57 |                 scales.width = abs(target.width / rect.width)
58 |                 scales.height = abs(target.height / rect.height)
59 |                 
60 |                 switch self {
61 |                 case .aspectFit:
62 |                     scales.width = min(scales.width, scales.height)
63 |                     scales.height = scales.width
64 |                 case .aspectFill:
65 |                     scales.width = max(scales.width, scales.height)
66 |                     scales.height = scales.width
67 |                 case .stretch:
68 |                     break
69 |                 case .center:
70 |                     scales.width = 1
71 |                     scales.height = 1
72 |                 }
73 |                 
74 |                 var result = rect.standardized
75 |                 result.size.width *= scales.width
76 |                 result.size.height *= scales.height
77 |                 result.origin.x = target.minX + (target.width - result.width) / 2
78 |                 result.origin.y = target.minY + (target.height - result.height) / 2
79 |                 return result
80 |             }
81 |         }
82 |     }
83 | }
84 | 
85 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Icons/AlertSpinnerView.swift:
--------------------------------------------------------------------------------
 1 | import UIKit
 2 | 
 3 | class AlertSpinnerView: UIView {
 4 |     
 5 |     let activityIndicatorView: UIActivityIndicatorView
 6 |     
 7 |     init(style: UIActivityIndicatorView.Style) {
 8 |         self.activityIndicatorView = UIActivityIndicatorView(style: style)
 9 |         super.init(frame: .zero)
10 |         self.backgroundColor = .clear
11 |         addSubview(activityIndicatorView)
12 |         activityIndicatorView.startAnimating()
13 |     }
14 |     
15 |     required init?(coder aDecoder: NSCoder) {
16 |         fatalError("init(coder:) has not been implemented")
17 |     }
18 |     
19 |     override func layoutSubviews() {
20 |         super.layoutSubviews()
21 |         activityIndicatorView.sizeToFit()
22 |         activityIndicatorView.center = .init(x: frame.width / 2, y: frame.height / 2)
23 |     }
24 | 
25 | }
26 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Views/AlertAppleMusic16View.swift:
--------------------------------------------------------------------------------
  1 | import UIKit
  2 | 
  3 | @available(iOS 13, *)
  4 | public class AlertAppleMusic16View: UIView, AlertViewProtocol {
  5 |     
  6 |     open var dismissByTap: Bool = true
  7 |     open var dismissInTime: Bool = true
  8 |     open var duration: TimeInterval = 1.5
  9 |     open var haptic: AlertHaptic? = nil
 10 |     
 11 |     public let titleLabel: UILabel?
 12 |     public let subtitleLabel: UILabel?
 13 |     public let iconView: UIView?
 14 |     
 15 |     public static var defaultContentColor = UIColor { trait in
 16 |         switch trait.userInterfaceStyle {
 17 |         case .dark: return UIColor(red: 127 / 255, green: 127 / 255, blue: 129 / 255, alpha: 1)
 18 |         default: return UIColor(red: 88 / 255, green: 87 / 255, blue: 88 / 255, alpha: 1)
 19 |         }
 20 |     }
 21 |     
 22 |     fileprivate weak var viewForPresent: UIView?
 23 |     fileprivate var presentDismissDuration: TimeInterval = 0.2
 24 |     fileprivate var presentDismissScale: CGFloat = 0.8
 25 |     
 26 |     fileprivate var completion: (()->Void)? = nil
 27 |     
 28 |     private lazy var backgroundView: UIVisualEffectView = {
 29 |         let view: UIVisualEffectView = {
 30 |             #if !os(tvOS)
 31 |             if #available(iOS 13.0, *) {
 32 |                 return UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
 33 |             } else {
 34 |                 return UIVisualEffectView(effect: UIBlurEffect(style: .light))
 35 |             }
 36 |             #else
 37 |             return UIVisualEffectView(effect: UIBlurEffect(style: .light))
 38 |             #endif
 39 |         }()
 40 |         view.isUserInteractionEnabled = false
 41 |         return view
 42 |     }()
 43 |     
 44 |     public init(title: String? = nil, subtitle: String? = nil, icon: AlertIcon? = nil) {
 45 |         
 46 |         if let title = title {
 47 |             let label = UILabel()
 48 |             label.font = UIFont.preferredFont(forTextStyle: .title2, weight: .bold)
 49 |             label.numberOfLines = 0
 50 |             let style = NSMutableParagraphStyle()
 51 |             style.lineSpacing = 3
 52 |             style.alignment = .center
 53 |             label.attributedText = NSAttributedString(string: title, attributes: [.paragraphStyle: style])
 54 |             titleLabel = label
 55 |         } else {
 56 |             self.titleLabel = nil
 57 |         }
 58 |         
 59 |         if let subtitle = subtitle {
 60 |             let label = UILabel()
 61 |             label.font = UIFont.preferredFont(forTextStyle: .body)
 62 |             label.numberOfLines = 0
 63 |             let style = NSMutableParagraphStyle()
 64 |             style.lineSpacing = 2
 65 |             style.alignment = .center
 66 |             label.attributedText = NSAttributedString(string: subtitle, attributes: [.paragraphStyle: style])
 67 |             subtitleLabel = label
 68 |         } else {
 69 |             self.subtitleLabel = nil
 70 |         }
 71 |         
 72 |         if let icon = icon {
 73 |             let view = icon.createView(lineThick: 9)
 74 |             self.iconView = view
 75 |         } else {
 76 |             self.iconView = nil
 77 |         }
 78 |         
 79 |         if icon == nil {
 80 |             layout = AlertLayout.message()
 81 |         } else {
 82 |             layout = AlertLayout(for: icon ?? .heart)
 83 |         }
 84 |         
 85 |         self.titleLabel?.textColor = Self.defaultContentColor
 86 |         self.subtitleLabel?.textColor = Self.defaultContentColor
 87 |         self.iconView?.tintColor = Self.defaultContentColor
 88 |         
 89 |         super.init(frame: .zero)
 90 |         
 91 |         preservesSuperviewLayoutMargins = false
 92 |         insetsLayoutMarginsFromSafeArea = false
 93 |         
 94 |         backgroundColor = .clear
 95 |         addSubview(backgroundView)
 96 |         
 97 |         if let titleLabel = self.titleLabel {
 98 |             addSubview(titleLabel)
 99 |         }
100 |         if let subtitleLabel = self.subtitleLabel {
101 |             addSubview(subtitleLabel)
102 |         }
103 |         if let iconView = self.iconView {
104 |             addSubview(iconView)
105 |         }
106 |         
107 |         layoutMargins = layout.margins
108 |         
109 |         layer.masksToBounds = true
110 |         layer.cornerRadius = 8
111 |         layer.cornerCurve = .continuous
112 |         
113 |         switch icon {
114 |         case .spinnerSmall, .spinnerLarge:
115 |             dismissInTime = false
116 |             dismissByTap = false
117 |         default:
118 |             dismissInTime = true
119 |             dismissByTap = true
120 |         }
121 |     }
122 |     
123 |     required init?(coder: NSCoder) {
124 |         fatalError("init(coder:) has not been implemented")
125 |     }
126 |     
127 |     open func present(on view: UIView, completion: (()->Void)? = nil) {
128 |         self.viewForPresent = view
129 |         self.completion = completion
130 |         viewForPresent?.addSubview(self)
131 |         guard let viewForPresent = viewForPresent else { return }
132 |         
133 |         alpha = 0
134 |         sizeToFit()
135 |         center = .init(x: viewForPresent.frame.midX, y: viewForPresent.frame.midY)
136 |         transform = transform.scaledBy(x: self.presentDismissScale, y: self.presentDismissScale)
137 |         
138 |         if dismissByTap {
139 |             let tapGesterRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismiss))
140 |             addGestureRecognizer(tapGesterRecognizer)
141 |         }
142 |         
143 |         // Present
144 |         
145 |         haptic?.impact()
146 |         
147 |         UIView.animate(withDuration: presentDismissDuration, animations: {
148 |             self.alpha = 1
149 |             self.transform = CGAffineTransform.identity
150 |         }, completion: { [weak self] finished in
151 |             guard let self = self else { return }
152 |             
153 |             if let iconView = self.iconView as? AlertIconAnimatable {
154 |                 iconView.animate()
155 |             }
156 |             
157 |             if self.dismissInTime {
158 |                 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + self.duration) {
159 |                     // If dismiss manually no need call original completion.
160 |                     if self.alpha != 0 {
161 |                         self.dismiss()
162 |                     }
163 |                 }
164 |             }
165 |         })
166 |     }
167 |     
168 |     @objc open func dismiss() {
169 |         self.dismiss(customCompletion: self.completion)
170 |     }
171 |     
172 |     func dismiss(customCompletion: (()->Void)? = nil) {
173 |         UIView.animate(withDuration: presentDismissDuration, animations: {
174 |             self.alpha = 0
175 |             self.transform = self.transform.scaledBy(x: self.presentDismissScale, y: self.presentDismissScale)
176 |         }, completion: { [weak self] finished in
177 |             self?.removeFromSuperview()
178 |             customCompletion?()
179 |         })
180 |     }
181 |     
182 |     private let layout: AlertLayout
183 |     
184 |     public override func layoutSubviews() {
185 |         super.layoutSubviews()
186 |         
187 |         guard self.transform == .identity else { return }
188 |         backgroundView.frame = self.bounds
189 |         
190 |         if let iconView = self.iconView {
191 |             iconView.frame = .init(origin: .init(x: 0, y: layoutMargins.top), size: layout.iconSize)
192 |             iconView.center.x = bounds.midX
193 |         }
194 |         if let titleLabel = self.titleLabel {
195 |             titleLabel.layoutDynamicHeight(
196 |                 x: layoutMargins.left,
197 |                 y: iconView == nil ? layoutMargins.top : (iconView?.frame.maxY ?? 0) + layout.spaceBetweenIconAndTitle,
198 |                 width: frame.width - layoutMargins.left - layoutMargins.right)
199 |         }
200 |         if let subtitleLabel = self.subtitleLabel {
201 |             let yPosition: CGFloat = {
202 |                 if let titleLabel = self.titleLabel {
203 |                     return titleLabel.frame.maxY + 4
204 |                 } else {
205 |                     return layoutMargins.top
206 |                 }
207 |             }()
208 |             subtitleLabel.layoutDynamicHeight(x: layoutMargins.left, y: yPosition, width: frame.width - layoutMargins.left - layoutMargins.right)
209 |         }
210 |     }
211 |     
212 |     public override func sizeThatFits(_ size: CGSize) -> CGSize {
213 |         let width: CGFloat = 250
214 |         self.frame = .init(x: frame.origin.x, y: frame.origin.y, width: width, height: frame.height)
215 |         layoutSubviews()
216 |         let height = subtitleLabel?.frame.maxY ?? titleLabel?.frame.maxY ?? iconView?.frame.maxY ?? .zero
217 |         return .init(width: width, height: height + layoutMargins.bottom)
218 |     }
219 |     
220 |     private class AlertLayout {
221 |         
222 |         var iconSize: CGSize
223 |         var margins: UIEdgeInsets
224 |         var spaceBetweenIconAndTitle: CGFloat
225 |         
226 |         public init(iconSize: CGSize, margins: UIEdgeInsets, spaceBetweenIconAndTitle: CGFloat) {
227 |             self.iconSize = iconSize
228 |             self.margins = margins
229 |             self.spaceBetweenIconAndTitle = spaceBetweenIconAndTitle
230 |         }
231 |         
232 |         convenience init() {
233 |             self.init(iconSize: .init(width: 100, height: 100), margins: .init(top: 43, left: 16, bottom: 25, right: 16), spaceBetweenIconAndTitle: 41)
234 |         }
235 |         
236 |         static func message() -> AlertLayout {
237 |             let layout = AlertLayout()
238 |             layout.margins = UIEdgeInsets(top: 23, left: 16, bottom: 23, right: 16)
239 |             return layout
240 |         }
241 |         
242 |         convenience init(for preset: AlertIcon) {
243 |             switch preset {
244 |             case .done:
245 |                 self.init(
246 |                     iconSize: .init(
247 |                         width: 112,
248 |                         height: 112
249 |                     ),
250 |                     margins: .init(
251 |                         top: 63,
252 |                         left: Self.defaultHorizontalInset,
253 |                         bottom: 29,
254 |                         right: Self.defaultHorizontalInset
255 |                     ),
256 |                     spaceBetweenIconAndTitle: 35
257 |                 )
258 |             case .heart:
259 |                 self.init(
260 |                     iconSize: .init(
261 |                         width: 112,
262 |                         height: 77
263 |                     ),
264 |                     margins: .init(
265 |                         top: 49,
266 |                         left: Self.defaultHorizontalInset,
267 |                         bottom: 25,
268 |                         right: Self.defaultHorizontalInset
269 |                     ),
270 |                     spaceBetweenIconAndTitle: 35
271 |                 )
272 |             case .error:
273 |                 self.init(
274 |                     iconSize: .init(
275 |                         width: 86,
276 |                         height: 86
277 |                     ),
278 |                     margins: .init(
279 |                         top: 63,
280 |                         left: Self.defaultHorizontalInset,
281 |                         bottom: 29,
282 |                         right: Self.defaultHorizontalInset
283 |                     ),
284 |                     spaceBetweenIconAndTitle: 39
285 |                 )
286 |             case .spinnerLarge, .spinnerSmall:
287 |                 self.init(
288 |                     iconSize: .init(
289 |                         width: 16,
290 |                         height: 16
291 |                     ),
292 |                     margins: .init(
293 |                         top: 58,
294 |                         left: Self.defaultHorizontalInset,
295 |                         bottom: 27,
296 |                         right: Self.defaultHorizontalInset
297 |                     ),
298 |                     spaceBetweenIconAndTitle: 39
299 |                 )
300 |             case .custom(_):
301 |                 self.init(
302 |                     iconSize: .init(
303 |                         width: 100,
304 |                         height: 100
305 |                     ),
306 |                     margins: .init(
307 |                         top: 43,
308 |                         left: Self.defaultHorizontalInset,
309 |                         bottom: 25,
310 |                         right: Self.defaultHorizontalInset
311 |                     ),
312 |                     spaceBetweenIconAndTitle: 35
313 |                 )
314 |             }
315 |         }
316 |         
317 |         private static var defaultHorizontalInset: CGFloat { return 16 }
318 |     }
319 | }
320 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Views/AlertAppleMusic17View.swift:
--------------------------------------------------------------------------------
  1 | import UIKit
  2 | import SwiftUI
  3 | 
  4 | @available(iOS 13, visionOS 1, *)
  5 | public class AlertAppleMusic17View: UIView, AlertViewProtocol, AlertViewInternalDismissProtocol {
  6 |     
  7 |     open var dismissByTap: Bool = true
  8 |     open var dismissInTime: Bool = true
  9 |     open var duration: TimeInterval = 1.5
 10 |     open var haptic: AlertHaptic? = nil
 11 |     
 12 |     public let titleLabel: UILabel?
 13 |     public let subtitleLabel: UILabel?
 14 |     public let iconView: UIView?
 15 |     
 16 |     public static var defaultContentColor = UIColor { trait in
 17 |         #if os(visionOS)
 18 |         return .label
 19 |         #else
 20 |         switch trait.userInterfaceStyle {
 21 |         case .dark: return UIColor(red: 127 / 255, green: 127 / 255, blue: 129 / 255, alpha: 1)
 22 |         default: return UIColor(red: 88 / 255, green: 87 / 255, blue: 88 / 255, alpha: 1)
 23 |         }
 24 |         #endif
 25 |     }
 26 |     
 27 |     fileprivate weak var viewForPresent: UIView?
 28 |     fileprivate var presentDismissDuration: TimeInterval = 0.2
 29 |     fileprivate var presentDismissScale: CGFloat = 0.8
 30 |     
 31 |     fileprivate var completion: (()->Void)? = nil
 32 |     
 33 |     private lazy var backgroundView: UIView = {
 34 |         #if os(visionOS)
 35 |         let swiftUIView = VisionGlassBackgroundView(cornerRadius: 12)
 36 |         let host = UIHostingController(rootView: swiftUIView)
 37 |         let hostView = host.view ?? UIView()
 38 |         hostView.isUserInteractionEnabled = false
 39 |         return hostView
 40 |         #else
 41 |         let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
 42 |         view.isUserInteractionEnabled = false
 43 |         return view
 44 |         #endif
 45 |     }()
 46 |     
 47 |     public init(title: String? = nil, subtitle: String? = nil, icon: AlertIcon? = nil) {
 48 |         
 49 |         if let title = title {
 50 |             let label = UILabel()
 51 |             label.font = UIFont.preferredFont(forTextStyle: .body, weight: .semibold, addPoints: -2)
 52 |             label.numberOfLines = 0
 53 |             let style = NSMutableParagraphStyle()
 54 |             style.lineSpacing = 3
 55 |             style.alignment = .left
 56 |             label.attributedText = NSAttributedString(string: title, attributes: [.paragraphStyle: style])
 57 |             titleLabel = label
 58 |         } else {
 59 |             self.titleLabel = nil
 60 |         }
 61 |         
 62 |         if let subtitle = subtitle {
 63 |             let label = UILabel()
 64 |             label.font = UIFont.preferredFont(forTextStyle: .footnote)
 65 |             label.numberOfLines = 0
 66 |             let style = NSMutableParagraphStyle()
 67 |             style.lineSpacing = 2
 68 |             style.alignment = .left
 69 |             label.attributedText = NSAttributedString(string: subtitle, attributes: [.paragraphStyle: style])
 70 |             subtitleLabel = label
 71 |         } else {
 72 |             self.subtitleLabel = nil
 73 |         }
 74 |         
 75 |         if let icon = icon {
 76 |             let view = icon.createView(lineThick: 3)
 77 |             self.iconView = view
 78 |         } else {
 79 |             self.iconView = nil
 80 |         }
 81 |         
 82 |         self.titleLabel?.textColor = Self.defaultContentColor
 83 |         self.subtitleLabel?.textColor = Self.defaultContentColor
 84 |         self.iconView?.tintColor = Self.defaultContentColor
 85 |         
 86 |         super.init(frame: .zero)
 87 |         
 88 |         preservesSuperviewLayoutMargins = false
 89 |         insetsLayoutMarginsFromSafeArea = false
 90 |         
 91 |         backgroundColor = .clear
 92 |         addSubview(backgroundView)
 93 |         
 94 |         if let titleLabel = self.titleLabel {
 95 |             addSubview(titleLabel)
 96 |         }
 97 |         if let subtitleLabel = self.subtitleLabel {
 98 |             addSubview(subtitleLabel)
 99 |         }
100 |         
101 |         if let iconView = self.iconView {
102 |             addSubview(iconView)
103 |         }
104 |         
105 |         if subtitleLabel == nil {
106 |             layoutMargins = .init(top: 17, left: 15, bottom: 17, right: 15 + ((icon == nil) ? .zero : 3))
107 |         } else {
108 |             layoutMargins = .init(top: 15, left: 15, bottom: 15, right: 15 + ((icon == nil) ? .zero : 3))
109 |         }
110 |         
111 |         layer.masksToBounds = true
112 |         layer.cornerRadius = 14
113 |         layer.cornerCurve = .continuous
114 |         
115 |         switch icon {
116 |         case .spinnerSmall, .spinnerLarge:
117 |             dismissInTime = false
118 |             dismissByTap = false
119 |         default:
120 |             dismissInTime = true
121 |             dismissByTap = true
122 |         }
123 |     }
124 |     
125 |     required init?(coder: NSCoder) {
126 |         fatalError("init(coder:) has not been implemented")
127 |     }
128 |     
129 |     open func present(on view: UIView, completion: (()->Void)? = nil) {
130 |         self.viewForPresent = view
131 |         self.completion = completion
132 |         viewForPresent?.addSubview(self)
133 |         guard let viewForPresent = viewForPresent else { return }
134 |         
135 |         alpha = 0
136 |         sizeToFit()
137 |         center.x = viewForPresent.frame.midX
138 |         #if os(visionOS)
139 |         frame.origin.y = viewForPresent.safeAreaInsets.top + 24
140 |         #elseif os(iOS)
141 |         frame.origin.y = viewForPresent.frame.height - viewForPresent.safeAreaInsets.bottom - frame.height - 64
142 |         #endif
143 |         
144 |         transform = transform.scaledBy(x: self.presentDismissScale, y: self.presentDismissScale)
145 |         
146 |         if dismissByTap {
147 |             let tapGesterRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismiss))
148 |             addGestureRecognizer(tapGesterRecognizer)
149 |         }
150 |         
151 |         // Present
152 |         
153 |         haptic?.impact()
154 |         
155 |         UIView.animate(withDuration: presentDismissDuration, animations: {
156 |             self.alpha = 1
157 |             self.transform = CGAffineTransform.identity
158 |         }, completion: { [weak self] finished in
159 |             guard let self = self else { return }
160 |             
161 |             if let iconView = self.iconView as? AlertIconAnimatable {
162 |                 iconView.animate()
163 |             }
164 |             
165 |             if self.dismissInTime {
166 |                 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + self.duration) {
167 |                     // If dismiss manually no need call original completion.
168 |                     if self.alpha != 0 {
169 |                         self.dismiss()
170 |                     }
171 |                 }
172 |             }
173 |         })
174 |     }
175 |     
176 |     @objc open func dismiss() {
177 |         self.dismiss(customCompletion: self.completion)
178 |     }
179 |     
180 |     func dismiss(customCompletion: (()->Void)? = nil) {
181 |         UIView.animate(withDuration: presentDismissDuration, animations: {
182 |             self.alpha = 0
183 |             self.transform = self.transform.scaledBy(x: self.presentDismissScale, y: self.presentDismissScale)
184 |         }, completion: { [weak self] finished in
185 |             self?.removeFromSuperview()
186 |             customCompletion?()
187 |         })
188 |     }
189 |     
190 |     public override func layoutSubviews() {
191 |         super.layoutSubviews()
192 |         guard self.transform == .identity else { return }
193 |         backgroundView.frame = self.bounds
194 |         layout(maxWidth: frame.width)
195 |     }
196 |     
197 |     public override func sizeThatFits(_ size: CGSize) -> CGSize {
198 |         layout(maxWidth: nil)
199 |         
200 |         let maxX = subviews.sorted(by: { $0.frame.maxX > $1.frame.maxX }).first?.frame.maxX ?? .zero
201 |         let currentNeedWidth = maxX + layoutMargins.right
202 |         
203 |         let maxWidth = {
204 |             if let viewForPresent = self.viewForPresent {
205 |                 return min(viewForPresent.frame.width * 0.8, 270)
206 |             } else {
207 |                 return 270
208 |             }
209 |         }()
210 |         
211 |         let usingWidth = min(currentNeedWidth, maxWidth)
212 |         layout(maxWidth: usingWidth)
213 |         let height = subtitleLabel?.frame.maxY ?? titleLabel?.frame.maxY ?? .zero
214 |         return .init(width: usingWidth, height: height + layoutMargins.bottom)
215 |     }
216 |     
217 |     private func layout(maxWidth: CGFloat?) {
218 |         
219 |         let spaceBetweenLabelAndIcon: CGFloat = 12
220 |         let spaceBetweenTitleAndSubtitle: CGFloat = 4
221 |         
222 |         if let iconView = self.iconView {
223 |             iconView.frame = .init(x: layoutMargins.left, y: .zero, width: 20, height: 20)
224 |             let xPosition = iconView.frame.maxX + spaceBetweenLabelAndIcon
225 |             if let maxWidth = maxWidth {
226 |                 let labelWidth = maxWidth - xPosition - layoutMargins.right
227 |                 titleLabel?.frame = .init(
228 |                     x: xPosition,
229 |                     y: layoutMargins.top,
230 |                     width: labelWidth,
231 |                     height: titleLabel?.frame.height ?? .zero
232 |                 )
233 |                 titleLabel?.sizeToFit()
234 |                 subtitleLabel?.frame = .init(
235 |                     x: xPosition,
236 |                     y: (titleLabel?.frame.maxY ?? layoutMargins.top) + spaceBetweenTitleAndSubtitle,
237 |                     width: labelWidth,
238 |                     height: subtitleLabel?.frame.height ?? .zero
239 |                 )
240 |                 subtitleLabel?.sizeToFit()
241 |             } else {
242 |                 titleLabel?.sizeToFit()
243 |                 titleLabel?.frame.origin.x = xPosition
244 |                 titleLabel?.frame.origin.y = layoutMargins.top
245 |                 subtitleLabel?.sizeToFit()
246 |                 subtitleLabel?.frame.origin.x = xPosition
247 |                 subtitleLabel?.frame.origin.y = (titleLabel?.frame.maxY ?? layoutMargins.top) + spaceBetweenTitleAndSubtitle
248 |             }
249 |         } else {
250 |             if let maxWidth = maxWidth {
251 |                 let labelWidth = maxWidth - layoutMargins.left - layoutMargins.right
252 |                 titleLabel?.frame = .init(
253 |                     x: layoutMargins.left,
254 |                     y: layoutMargins.top,
255 |                     width: labelWidth,
256 |                     height: titleLabel?.frame.height ?? .zero
257 |                 )
258 |                 titleLabel?.sizeToFit()
259 |                 subtitleLabel?.frame = .init(
260 |                     x: layoutMargins.left,
261 |                     y: (titleLabel?.frame.maxY ?? layoutMargins.top) + spaceBetweenTitleAndSubtitle,
262 |                     width: labelWidth,
263 |                     height: subtitleLabel?.frame.height ?? .zero
264 |                 )
265 |                 subtitleLabel?.sizeToFit()
266 |             } else {
267 |                 titleLabel?.sizeToFit()
268 |                 titleLabel?.frame.origin.x = layoutMargins.left
269 |                 titleLabel?.frame.origin.y = layoutMargins.top
270 |                 subtitleLabel?.sizeToFit()
271 |                 subtitleLabel?.frame.origin.x = layoutMargins.left
272 |                 subtitleLabel?.frame.origin.y = (titleLabel?.frame.maxY ?? layoutMargins.top) + spaceBetweenTitleAndSubtitle
273 |             }
274 |         }
275 |         
276 |         iconView?.center.y = frame.height / 2
277 |     }
278 |     
279 |     #if os(visionOS)
280 |     struct VisionGlassBackgroundView: View {
281 |         
282 |         let cornerRadius: CGFloat
283 |         
284 |         var body: some View {
285 |             ZStack {
286 |                 Color.clear
287 |             }
288 |             .glassBackgroundEffect(in: .rect(cornerRadius: cornerRadius))
289 |             .opacity(0.4)
290 |         }
291 |     }
292 |     #endif
293 | }
294 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Views/AlertViewInternalDismissProtocol.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | 
3 | protocol AlertViewInternalDismissProtocol {
4 |     
5 |     func dismiss(customCompletion: (()->Void)?)
6 | }
7 | 
--------------------------------------------------------------------------------
/Sources/AlertKit/Views/AlertViewProtocol.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | 
3 | public protocol AlertViewProtocol {
4 |     
5 |     func present(on view: UIView, completion: (()->Void)?)
6 |     func dismiss()
7 | }
8 | 
--------------------------------------------------------------------------------