├── .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 | ![Alert Kit v5](https://cdn.sparrowcode.io/github/alertkit/v5/preview-v1_2.png) 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 | --------------------------------------------------------------------------------