├── .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 |
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 |
83 |
84 | ### Manually
85 |
86 | If you prefer not to use any of dependency managers, you can integrate manually. Put `Sources/AlertKit` folder in your Xcode project. Make sure to enable `Copy items if needed` and `Create groups`.
87 |
88 | ## SwiftUI
89 |
90 | You can use basic way via `AlertKitAPI` or call via modifier:
91 |
92 | ```swift
93 | let alertView = AlertAppleMusic17View(title: "Hello", subtitle: nil, icon: .done)
94 |
95 | VStack {}
96 | .alert(isPresent: $alertPresented, view: alertView)
97 | ```
98 |
99 | ## Customisation
100 |
101 | If you need customisation fonts, icon, colors or any other, make view:
102 |
103 | ```swift
104 | let alertView = AlertAppleMusic17View(title: "Added to Library", subtitle: nil, icon: .done)
105 |
106 | // change font
107 | alertView.titleLabel.font = UIFont.systemFont(ofSize: 21)
108 | // change color
109 | alertView.titleLabel.textColor = .white
110 | ```
111 |
112 | ## Present & Dismiss
113 |
114 | You can present and dismiss alerts manually via view.
115 |
116 | ```swift
117 | let alertView = AlertAppleMusic17View(title: "Added to Library", subtitle: nil, icon: .done)
118 |
119 | // present
120 | alertView.present(on: self)
121 | // and dismiss
122 | alertView.dismiss()
123 | ```
124 |
125 | For dismiss all alerts that was presented:
126 |
127 | ```swift
128 | AlertKitAPI.dismissAllAlerts()
129 | ```
130 |
131 | ## Apps Using
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
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 |
--------------------------------------------------------------------------------