diff --git a/Drops.podspec b/Drops.podspec index aed5c86..b1d1835 100644 --- a/Drops.podspec +++ b/Drops.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Drops' - s.version = '1.4.0' + s.version = '1.5.0' s.summary = 'A µFramework for showing iOS 13 like system alerts' s.description = <<-DESC A µFramework for showing alerts like the one used when copying from pasteboard or connecting Apple pencil. @@ -15,5 +15,5 @@ Pod::Spec.new do |s| s.source_files = 'Sources/**/*.swift' s.swift_versions = ['5.1', '5.2', '5.3'] s.requires_arc = true - s.ios.deployment_target = '10.0' + s.ios.deployment_target = '13.0' end diff --git a/Drops.xcodeproj/project.pbxproj b/Drops.xcodeproj/project.pbxproj index 01b2730..2ecf221 100644 --- a/Drops.xcodeproj/project.pbxproj +++ b/Drops.xcodeproj/project.pbxproj @@ -23,7 +23,6 @@ 074942172642D46E0031D338 /* Drops.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07D8146D26361DB7003F51DB /* Drops.framework */; }; 074942182642D46E0031D338 /* Drops.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 07D8146D26361DB7003F51DB /* Drops.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0755C206263B5EFA002209FD /* DropTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0755C205263B5EFA002209FD /* DropTests.swift */; }; - 077DCFD2264B2C4000BF2447 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077DCFD1264B2C4000BF2447 /* UIView+Extensions.swift */; }; 0785D38426416EF3004D6171 /* PassthroughWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0785D37926416D84004D6171 /* PassthroughWindow.swift */; }; 0785D38526416EF3004D6171 /* PassthroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0785D37726416D6A004D6171 /* PassthroughView.swift */; }; 0785D38626416EF3004D6171 /* AnimationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0785D37F26416E0E004D6171 /* AnimationContext.swift */; }; @@ -109,7 +108,6 @@ 074942132642D4160031D338 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0755C1E1263A4EF0002209FD /* DropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropView.swift; sourceTree = ""; }; 0755C205263B5EFA002209FD /* DropTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropTests.swift; sourceTree = ""; }; - 077DCFD1264B2C4000BF2447 /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; 0785D3732640BBF7004D6171 /* Drops.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drops.swift; sourceTree = ""; }; 0785D37726416D6A004D6171 /* PassthroughView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassthroughView.swift; sourceTree = ""; }; 0785D37926416D84004D6171 /* PassthroughWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassthroughWindow.swift; sourceTree = ""; }; @@ -233,7 +231,6 @@ 0785D37726416D6A004D6171 /* PassthroughView.swift */, 0785D37926416D84004D6171 /* PassthroughWindow.swift */, 0785D38C2641F487004D6171 /* Presenter.swift */, - 077DCFD1264B2C4000BF2447 /* UIView+Extensions.swift */, 0785D37B26416DB3004D6171 /* Weak.swift */, 0785D37D26416DDF004D6171 /* WindowViewController.swift */, ); @@ -499,7 +496,6 @@ 0785D38726416EF3004D6171 /* Drop.swift in Sources */, 0785D38826416EF3004D6171 /* Animator.swift in Sources */, 0785D38626416EF3004D6171 /* AnimationContext.swift in Sources */, - 077DCFD2264B2C4000BF2447 /* UIView+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -558,6 +554,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = C3VKVFB3SA; INFOPLIST_FILE = UIKitExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -578,6 +575,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = C3VKVFB3SA; INFOPLIST_FILE = UIKitExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -598,6 +596,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = C3VKVFB3SA; ENABLE_PREVIEWS = NO; INFOPLIST_FILE = SwiftUIExample/Info.plist; @@ -619,6 +618,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = C3VKVFB3SA; ENABLE_PREVIEWS = NO; INFOPLIST_FILE = SwiftUIExample/Info.plist; @@ -686,7 +686,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -744,7 +744,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -768,7 +768,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -797,7 +797,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Package.swift b/Package.swift index 41281c8..df876ab 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ import PackageDescription let package = Package( name: "Drops", platforms: [ - .iOS(.v10) + .iOS(.v13) ], products: [ .library(name: "Drops", targets: ["Drops"]) diff --git a/README.md b/README.md index cb72b68..16deda8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A µFramework for showing alerts like the one used when copying from pasteboard [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fomaralbeik%2FDrops%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/omaralbeik/Drops) ## Features -- iOS 10+ +- iOS 13+ - Can be used in SwiftUI and UIKit applications - Light/Dark modes - Interactive dismissal @@ -119,7 +119,7 @@ The [Swift Package Manager](https://swift.org/package-manager/) is a tool for ma ```swift dependencies: [ - .package(url: "https://github.com/omaralbeik/Drops.git", from: "1.4.0") + .package(url: "https://github.com/omaralbeik/Drops.git", from: "1.5.0") ] ``` @@ -134,7 +134,7 @@ $ swift build To integrate Drops into your Xcode project using [CocoaPods](https://cocoapods.org), specify it in your Podfile: ```rb -pod 'Drops', :git => 'https://github.com/omaralbeik/Drops.git', :tag => '1.4.0' +pod 'Drops', :git => 'https://github.com/omaralbeik/Drops.git', :tag => '1.5.0' ``` ### Carthage diff --git a/Sources/Animator.swift b/Sources/Animator.swift index e024021..5b46532 100644 --- a/Sources/Animator.swift +++ b/Sources/Animator.swift @@ -36,18 +36,18 @@ internal final class Animator { var closePercent: CGFloat = 0.0 var panTranslationY: CGFloat = 0.0 } - + init(position: Drop.Position, delegate: AnimatorDelegate) { self.position = position self.delegate = delegate } - + let position: Drop.Position weak var delegate: AnimatorDelegate? - + var context: AnimationContext? var panState = PanState() - + let showDuration: TimeInterval = 0.75 let hideDuration: TimeInterval = 0.25 let springDamping: CGFloat = 0.8 @@ -56,59 +56,59 @@ internal final class Animator { let closePercentThreshold: CGFloat = 0.33 let closeAbsoluteThreshold: CGFloat = 75.0 let bounceOffset: CGFloat = 5 - + private lazy var panGestureRecognizer: UIPanGestureRecognizer = { let recognizer = UIPanGestureRecognizer() recognizer.addTarget(self, action: #selector(handlePan)) return recognizer }() - + func install(context: AnimationContext) { let view = context.view let container = context.container - + self.context = context - + view.translatesAutoresizingMaskIntoConstraints = false container.addSubview(view) - + var constraints = [ - view.centerXAnchor.constraint(equalTo: container.safeArea.centerXAnchor), - view.leadingAnchor.constraint(greaterThanOrEqualTo: container.safeArea.leadingAnchor, constant: 20), - view.trailingAnchor.constraint(lessThanOrEqualTo: container.safeArea.trailingAnchor, constant: -20), + view.centerXAnchor.constraint(equalTo: container.safeAreaLayoutGuide.centerXAnchor), + view.leadingAnchor.constraint(greaterThanOrEqualTo: container.safeAreaLayoutGuide.leadingAnchor, constant: 20), + view.trailingAnchor.constraint(lessThanOrEqualTo: container.safeAreaLayoutGuide.trailingAnchor, constant: -20) ] - + switch position { case .top: constraints += [ - view.topAnchor.constraint(equalTo: container.safeArea.topAnchor, constant: bounceOffset), + view.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor, constant: bounceOffset) ] case .bottom: constraints += [ - view.bottomAnchor.constraint(equalTo: container.safeArea.bottomAnchor, constant: -bounceOffset), + view.bottomAnchor.constraint(equalTo: container.safeAreaLayoutGuide.bottomAnchor, constant: -bounceOffset) ] } - + NSLayoutConstraint.activate(constraints) container.layoutIfNeeded() - + let animationDistance = view.frame.height - + switch position { case .top: view.transform = CGAffineTransform(translationX: 0, y: -animationDistance) case .bottom: view.transform = CGAffineTransform(translationX: 0, y: animationDistance) } - + view.addGestureRecognizer(panGestureRecognizer) } - + func show(context: AnimationContext, completion: @escaping AnimationCompletion) { install(context: context) show(completion: completion) } - + func hide(context: AnimationContext, completion: @escaping AnimationCompletion) { let position = self.position let view = context.view @@ -129,18 +129,18 @@ internal final class Animator { completion: completion ) } - + func show(completion: @escaping AnimationCompletion) { guard let view = context?.view else { completion(false) return } - + view.alpha = 0 - + let animationDistance = abs(view.transform.ty) let initialSpringVelocity = animationDistance == 0.0 ? 0.0 : min(0.0, panState.closeSpeed / animationDistance) - + UIView.animate( withDuration: showDuration, delay: 0.0, @@ -154,7 +154,7 @@ internal final class Animator { completion: completion ) } - + @objc func handlePan(gestureRecognizer: UIPanGestureRecognizer) { switch gestureRecognizer.state { @@ -175,59 +175,59 @@ internal final class Animator { break } } - + func panChanged(current: PanState, view: UIView, velocity: CGPoint, translation: CGPoint) -> PanState { var state = current var velocity = velocity var translation = translation let height = view.bounds.height - bounceOffset if height <= 0 { return state } - + if case .top = position { velocity.y *= -1.0 translation.y *= -1.0 } - + var translationAmount = translation.y >= 0 ? translation.y : -pow(abs(translation.y), 0.7) - + if !state.closing { if !rubberBanding, translationAmount < 0 { return state } state.closing = true delegate?.panStarted(animator: self) } - + if !rubberBanding, translationAmount < 0 { translationAmount = 0 } - + switch position { case .top: view.transform = CGAffineTransform(translationX: 0, y: -translationAmount) case .bottom: view.transform = CGAffineTransform(translationX: 0, y: translationAmount) } - + state.closeSpeed = velocity.y state.closePercent = translation.y / height state.panTranslationY = translation.y - + return state } - + func panEnded(current: PanState) -> PanState? { if current.closeSpeed > closeSpeedThreshold { delegate?.hide(animator: self) return nil } - + if current.closePercent > closePercentThreshold { delegate?.hide(animator: self) return nil } - + if current.panTranslationY > closeAbsoluteThreshold { delegate?.hide(animator: self) return nil } - + return .init() } } diff --git a/Sources/Drop.swift b/Sources/Drop.swift index cf0006c..819499b 100644 --- a/Sources/Drop.swift +++ b/Sources/Drop.swift @@ -63,7 +63,7 @@ public struct Drop: ExpressibleByStringLiteral { self.accessibility = accessibility ?? .init(message: [title, subtitle].compactMap { $0 }.joined(separator: ", ")) } - + /// Create a new accessibility object. /// - Parameter message: Message to be announced when the drop is shown. Defaults to drop's "title, subtitle" public init(stringLiteral title: String) { @@ -74,31 +74,31 @@ public struct Drop: ExpressibleByStringLiteral { duration = .recommended accessibility = .init(message: title) } - + /// Title. public var title: String - - /// Maximum number of lines that `title` can occupy. Defaults to `1`. A value of 0 means no limit. + + /// Maximum number of lines that `title` can occupy. A value of 0 means no limit. public var titleNumberOfLines: Int - + /// Subtitle. public var subtitle: String? - - /// Maximum number of lines that `subtitle` can occupy. Defaults to `1`. A value of 0 means no limit. + + /// Maximum number of lines that `subtitle` can occupy. A value of 0 means no limit. public var subtitleNumberOfLines: Int - + /// Icon. public var icon: UIImage? - + /// Action. public var action: Action? - + /// Position. public var position: Position - + /// Duration. public var duration: Duration - + /// Accessibility. public var accessibility: Accessibility } @@ -120,13 +120,13 @@ public extension Drop { case recommended /// Hides the drop after the specified number of seconds. case seconds(TimeInterval) - + /// Create a new duration object. /// - Parameter value: Duration in seconds public init(floatLiteral value: TimeInterval) { self = .seconds(value) } - + internal var value: TimeInterval { switch self { case .recommended: @@ -149,10 +149,10 @@ public extension Drop { self.icon = icon self.handler = handler } - + /// Icon. public var icon: UIImage? - + /// Handler. public var handler: () -> Void } @@ -166,13 +166,13 @@ public extension Drop { public init(message: String) { self.message = message } - + /// Create a new accessibility object. /// - Parameter message: Message to be announced when the drop is shown. Defaults to drop's "title, subtitle" public init(stringLiteral message: String) { self.message = message } - + /// Accessibility message to be announced when the drop is shown. public let message: String } diff --git a/Sources/DropView.swift b/Sources/DropView.swift index cd8ed50..fc418c4 100644 --- a/Sources/DropView.swift +++ b/Sources/DropView.swift @@ -28,11 +28,7 @@ internal final class DropView: UIView { self.drop = drop super.init(frame: .zero) - if #available(iOS 13.0, *) { - backgroundColor = .secondarySystemBackground - } else { - backgroundColor = .white - } + backgroundColor = .secondarySystemBackground addSubview(stackView) @@ -60,12 +56,12 @@ internal final class DropView: UIView { constraints += [ imageView.heightAnchor.constraint(equalToConstant: 25), - imageView.widthAnchor.constraint(equalToConstant: 25), + imageView.widthAnchor.constraint(equalToConstant: 25) ] constraints += [ button.heightAnchor.constraint(equalToConstant: 35), - button.widthAnchor.constraint(equalToConstant: 35), + button.widthAnchor.constraint(equalToConstant: 35) ] var insets = UIEdgeInsets(top: 7.5, left: 12.5, bottom: 7.5, right: 12.5) @@ -95,9 +91,9 @@ internal final class DropView: UIView { constraints += [ stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: insets.left), - stackView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: insets.top), + stackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: insets.top), stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -insets.right), - stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom) ] return constraints @@ -142,11 +138,7 @@ internal final class DropView: UIView { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textAlignment = .center - if #available(iOS 13.0, *) { - label.textColor = .label - } else { - label.textColor = .black - } + label.textColor = .label label.font = UIFont.preferredFont(forTextStyle: .subheadline).bold label.adjustsFontForContentSizeCategory = true label.adjustsFontSizeToFitWidth = true @@ -157,11 +149,7 @@ internal final class DropView: UIView { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textAlignment = .center - if #available(iOS 13.0, *) { - label.textColor = UIAccessibility.isDarkerSystemColorsEnabled ? .label : .secondaryLabel - } else { - label.textColor = UIAccessibility.isDarkerSystemColorsEnabled ? .black : .darkGray - } + label.textColor = UIAccessibility.isDarkerSystemColorsEnabled ? .label : .secondaryLabel label.font = UIFont.preferredFont(forTextStyle: .subheadline) label.adjustsFontForContentSizeCategory = true label.adjustsFontSizeToFitWidth = true @@ -173,11 +161,7 @@ internal final class DropView: UIView { view.translatesAutoresizingMaskIntoConstraints = false view.contentMode = .scaleAspectFit view.clipsToBounds = true - if #available(iOS 13.0, *) { - view.tintColor = UIAccessibility.isDarkerSystemColorsEnabled ? .label : .secondaryLabel - } else { - view.tintColor = UIAccessibility.isDarkerSystemColorsEnabled ? .black : .darkGray - } + view.tintColor = UIAccessibility.isDarkerSystemColorsEnabled ? .label : .secondaryLabel return view }() @@ -186,11 +170,7 @@ internal final class DropView: UIView { button.translatesAutoresizingMaskIntoConstraints = false button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) button.clipsToBounds = true - if #available(iOS 13.0, *) { - button.backgroundColor = .link - } else { - button.backgroundColor = .blue - } + button.backgroundColor = .link button.tintColor = .white button.imageView?.contentMode = .scaleAspectFit button.contentEdgeInsets = .init(top: 7.5, left: 7.5, bottom: 7.5, right: 7.5) diff --git a/Sources/Drops.swift b/Sources/Drops.swift index 815e4e2..91dac44 100644 --- a/Sources/Drops.swift +++ b/Sources/Drops.swift @@ -33,7 +33,7 @@ public final class Drops { // MARK: - Static - static let shared = Drops() + static var shared = Drops() /// Show a drop. /// - Parameter drop: `Drop` to show. diff --git a/Sources/Presenter.swift b/Sources/Presenter.swift index 1baef39..f15ae09 100644 --- a/Sources/Presenter.swift +++ b/Sources/Presenter.swift @@ -80,7 +80,7 @@ internal final class Presenter: NSObject { maskingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), maskingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), maskingView.topAnchor.constraint(equalTo: containerView.topAnchor), - maskingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + maskingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) ]) containerView.layoutIfNeeded() diff --git a/Sources/UIView+Extensions.swift b/Sources/UIView+Extensions.swift deleted file mode 100644 index 5d76373..0000000 --- a/Sources/UIView+Extensions.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Drops -// -// Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import UIKit - -internal extension UIView { - var safeArea: UILayoutGuide { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide - } else { - return layoutMarginsGuide - } - } -} diff --git a/Sources/WindowViewController.swift b/Sources/WindowViewController.swift index 039ba4c..5ddcbfa 100644 --- a/Sources/WindowViewController.swift +++ b/Sources/WindowViewController.swift @@ -40,20 +40,17 @@ internal final class WindowViewController: UIViewController { override var preferredStatusBarStyle: UIStatusBarStyle { // Workaround for https://github.com/omaralbeik/Drops/pull/22 let app = UIApplication.shared - let topViewController = app.keyWindow?.rootViewController?.top - return topViewController?.preferredStatusBarStyle ?? app.statusBarStyle + let windowScene = app.activeWindowScene + let topViewController = windowScene?.windows.first(where: \.isKeyWindow)?.rootViewController?.top + return topViewController?.preferredStatusBarStyle + ?? windowScene?.statusBarManager?.statusBarStyle + ?? .default } func install() { window?.frame = UIScreen.main.bounds window?.isHidden = false - if - let window = window, - #available(iOS 13, *), - let activeScene = UIApplication.shared.connectedScenes - .compactMap({ $0 as? UIWindowScene }) - .first(where: { $0.activationState == .foregroundActive }) - { + if let window = window, let activeScene = UIApplication.shared.activeWindowScene { window.windowScene = activeScene window.frame = activeScene.coordinateSpace.bounds } @@ -61,15 +58,21 @@ internal final class WindowViewController: UIViewController { func uninstall() { window?.isHidden = true - if #available(iOS 13, *) { - window?.windowScene = nil - } + window?.windowScene = nil window = nil } var window: UIWindow? } +private extension UIApplication { + var activeWindowScene: UIWindowScene? { + return connectedScenes + .compactMap { $0 as? UIWindowScene } + .first { $0.activationState == .foregroundActive } + } +} + private extension UIViewController { var top: UIViewController? { if let controller = self as? UINavigationController { diff --git a/SwiftUIExample/ContentView.swift b/SwiftUIExample/ContentView.swift index 3e184c2..884566c 100644 --- a/SwiftUIExample/ContentView.swift +++ b/SwiftUIExample/ContentView.swift @@ -25,14 +25,14 @@ import SwiftUI import Drops struct ContentView: View { - + @State var title: String = "Hello There!" @State var subtitle: String = "Use Drops to show alerts" @State var positionIndex: Int = 0 @State var duration: TimeInterval = 2.0 @State var hasIcon: Bool = false @State var hasActionIcon: Bool = false - + var body: some View { ZStack { Color(.secondarySystemBackground).ignoresSafeArea(.all) @@ -92,17 +92,17 @@ struct ContentView: View { UIApplication.shared.endEditing() } } - + private func showDrop() { UIApplication.shared.endEditing() - + let aTitle = title.trimmingCharacters(in: .whitespacesAndNewlines) let aSubtitle = subtitle.trimmingCharacters(in: .whitespacesAndNewlines) let position: Drop.Position = positionIndex == 0 ? .top : .bottom - + let icon = hasIcon ? UIImage(systemName: "star.fill") : nil let buttonIcon = hasActionIcon ? UIImage(systemName: "arrowshape.turn.up.left") : nil - + let drop = Drop( title: aTitle, subtitle: aSubtitle, diff --git a/Tests/AnimatorTests.swift b/Tests/AnimatorTests.swift index cd20c8f..3303201 100644 --- a/Tests/AnimatorTests.swift +++ b/Tests/AnimatorTests.swift @@ -53,10 +53,10 @@ final class AnimatorTests: XCTestCase { XCTAssertEqual(view.superview, container) var expectedConstraints: [NSLayoutConstraint] = [ - view.centerXAnchor.constraint(equalTo: container.safeArea.centerXAnchor), - view.leadingAnchor.constraint(greaterThanOrEqualTo: container.safeArea.leadingAnchor, constant: 20), - view.trailingAnchor.constraint(lessThanOrEqualTo: container.safeArea.trailingAnchor, constant: -20), - view.topAnchor.constraint(equalTo: container.safeArea.topAnchor, constant: animator.bounceOffset) + view.centerXAnchor.constraint(equalTo: container.safeAreaLayoutGuide.centerXAnchor), + view.leadingAnchor.constraint(greaterThanOrEqualTo: container.safeAreaLayoutGuide.leadingAnchor, constant: 20), + view.trailingAnchor.constraint(lessThanOrEqualTo: container.safeAreaLayoutGuide.trailingAnchor, constant: -20), + view.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor, constant: animator.bounceOffset) ] switch position { @@ -66,7 +66,7 @@ final class AnimatorTests: XCTestCase { case .bottom: expectedConstraints += [ view.bottomAnchor.constraint( - equalTo: container.safeArea.bottomAnchor, + equalTo: container.safeAreaLayoutGuide.bottomAnchor, constant: -animator.bounceOffset ) ] diff --git a/Tests/DropTests.swift b/Tests/DropTests.swift index 730edcf..bdc1a55 100644 --- a/Tests/DropTests.swift +++ b/Tests/DropTests.swift @@ -51,7 +51,6 @@ final class DropTests: XCTestCase { XCTAssertEqual(drop2.accessibility.message, "Alert: Hello world") } - @available(iOS 13.0, *) func testInitializer() { let icon = UIImage(systemName: "drop") let dismissIcon = UIImage(systemName: "xmark") diff --git a/Tests/DropViewTests.swift b/Tests/DropViewTests.swift index 1657c08..264ed49 100644 --- a/Tests/DropViewTests.swift +++ b/Tests/DropViewTests.swift @@ -29,13 +29,7 @@ final class DropViewTests: XCTestCase { let drop = Drop(title: "Test") let view = DropView(drop: drop) XCTAssertEqual(view.drop, drop) - - if #available(iOS 13.0, *) { - XCTAssertEqual(view.backgroundColor, .secondarySystemBackground) - } else { - XCTAssertEqual(view.backgroundColor, .white) - } - + XCTAssertEqual(view.backgroundColor, .secondarySystemBackground) XCTAssertFalse(view.constraints.isEmpty) XCTAssertFalse(view.subviews.isEmpty) @@ -53,7 +47,7 @@ final class DropViewTests: XCTestCase { view.button.heightAnchor.constraint(equalToConstant: 35), view.button.widthAnchor.constraint(equalToConstant: 35), view.stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50), - view.stackView.topAnchor.constraint(equalTo: view.safeArea.topAnchor, constant: 15), + view.stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15), view.stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50), view.stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -15) ] @@ -78,7 +72,7 @@ final class DropViewTests: XCTestCase { view.button.heightAnchor.constraint(equalToConstant: 35), view.button.widthAnchor.constraint(equalToConstant: 35), view.stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50), - view.stackView.topAnchor.constraint(equalTo: view.safeArea.topAnchor, constant: 7.5), + view.stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 7.5), view.stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50), view.stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -7.5) ] @@ -103,7 +97,7 @@ final class DropViewTests: XCTestCase { view.button.heightAnchor.constraint(equalToConstant: 35), view.button.widthAnchor.constraint(equalToConstant: 35), view.stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40), - view.stackView.topAnchor.constraint(equalTo: view.safeArea.topAnchor, constant: 10), + view.stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10), view.stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), view.stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10) ] diff --git a/Tests/DropsTests.swift b/Tests/DropsTests.swift index 78167cb..eb82bc1 100644 --- a/Tests/DropsTests.swift +++ b/Tests/DropsTests.swift @@ -25,97 +25,76 @@ import XCTest @testable import Drops final class DropsTests: XCTestCase { - func testShow() { + func testShow() async { let drops = Drops() let drop1 = Drop(title: "Test 1", duration: .seconds(1)) drops.show(drop1) - - let exp1 = expectation(description: "First Drops is presented") - DispatchQueue.main.async { - if drops.current != nil { - exp1.fulfill() - } + await Task.sleep(seconds: 0.1) + if drops.current == nil { + XCTFail("First drop is not presented") } - let exp2 = expectation(description: "First Drops is hidden") - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - if drops.current == nil { - exp2.fulfill() - } + await Task.sleep(seconds: 2) + if drops.current != nil { + XCTFail("First drop is not hidden") } - - waitForExpectations(timeout: 3) } - func testStaticShow() { + func testStaticShow() async { + Drops.shared = .init(delayBetweenDrops: 0) + let drop1 = Drop(title: "Test 1", duration: .seconds(1)) Drops.show(drop1) - let exp1 = expectation(description: "First Drops is presented") - DispatchQueue.main.async { - if Drops.shared.current != nil { - exp1.fulfill() - } + await Task.sleep(seconds: 0.1) + if Drops.shared.current == nil { + XCTFail("First drop is not presented") } - let exp2 = expectation(description: "First Drops is hidden") - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - if Drops.shared.current == nil { - exp2.fulfill() - } + await Task.sleep(seconds: 2) + if Drops.shared.current != nil { + XCTFail("First drop is not hidden") } - - waitForExpectations(timeout: 3) } - func testDropsAreQueued() { - let drops = Drops() + func testDropsAreQueued() async throws { + let drops = Drops(delayBetweenDrops: 0) (0..<5) - .map { Drop(title: "\($0)", duration: .seconds(0.5)) } + .map { Drop(title: "\($0)", duration: .seconds(0.1)) } .forEach(drops.show) - let exp1 = expectation(description: "All Drops are hidden") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if drops.queue.count == 5-1 { - exp1.fulfill() - } + await Task.sleep(seconds: 0.1) + if drops.queue.count != 4 { + XCTFail("First drop is not hidden") } - let exp2 = expectation(description: "All Drops are hidden") - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - if drops.queue.isEmpty { - exp2.fulfill() - } + await Task.sleep(seconds: 5) + if drops.queue.isEmpty == false { + XCTFail("All drops are not hidden") } - - waitForExpectations(timeout: 4) } - func testStaticDropsAreQueued() { + func testStaticDropsAreQueued() async { + Drops.shared = .init(delayBetweenDrops: 0) + (0..<5) - .map { Drop(title: "\($0)", duration: .seconds(0.5)) } + .map { Drop(title: "\($0)", duration: .seconds(0.1)) } .forEach(Drops.show) - let exp1 = expectation(description: "All Drops are hidden") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if Drops.shared.queue.count == 5-1 { - exp1.fulfill() - } + await Task.sleep(seconds: 0.1) + if Drops.shared.queue.count != 4 { + XCTFail("First drop is not hidden") } - let exp2 = expectation(description: "All Drops are hidden") - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - if Drops.shared.queue.isEmpty { - exp2.fulfill() - } + await Task.sleep(seconds: 5) + if Drops.shared.queue.isEmpty == false { + XCTFail("All drops are not hidden") } - - waitForExpectations(timeout: 4) } func testHideAll() { - let drops = Drops() + let drops = Drops(delayBetweenDrops: 0) (0..<10) .map { Drop(title: "\($0)", duration: .seconds(0.1)) } @@ -127,6 +106,8 @@ final class DropsTests: XCTestCase { } func testStaticHideAll() { + Drops.shared = .init(delayBetweenDrops: 0) + (0..<10) .map { Drop(title: "\($0)", duration: .seconds(0.1)) } .forEach(Drops.show) @@ -167,10 +148,12 @@ final class DropsTests: XCTestCase { drops.show(expectedDrop) - waitForExpectations(timeout: 1) + waitForExpectations(timeout: 2) } func testStaticHandlers() { + Drops.shared = .init(delayBetweenDrops: 0) + let expectedDrop = Drop(title: "Hello world!", duration: .seconds(0.1)) let willShowDropExp = expectation(description: "willShowDrop is called") @@ -200,7 +183,7 @@ final class DropsTests: XCTestCase { Drops.show(expectedDrop) - waitForExpectations(timeout: 1) + waitForExpectations(timeout: 2) } func testStaticHandlersSettersAndGetters() { @@ -245,3 +228,14 @@ final class DropsTests: XCTestCase { XCTAssertNil(Drops.didDismissDrop) } } + +private extension Task where Success == Never, Failure == Never { + static func sleep(seconds: Double) async { + let duration = UInt64(seconds * 1_000_000_000) + do { + try await Task.sleep(nanoseconds: duration) + } catch { + XCTFail(error.localizedDescription) + } + } +} diff --git a/Tests/WeakTests.swift b/Tests/WeakTests.swift index a49ef54..c373562 100644 --- a/Tests/WeakTests.swift +++ b/Tests/WeakTests.swift @@ -28,7 +28,7 @@ final class WeakTests: XCTestCase { func testWeak() { var instance: TestClass? = TestClass() let weak = Weak(value: instance) - + XCTAssertEqual(weak.value, instance) instance = nil