From d25a2bd6d0a7405cdfa083f1eaf3d7dffd1f96d8 Mon Sep 17 00:00:00 2001 From: Timothy Moose Date: Tue, 13 Jul 2021 15:06:36 -0500 Subject: [PATCH 1/8] Add dark mode theme --- SwiftMessages/MessageView.swift | 72 +++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/SwiftMessages/MessageView.swift b/SwiftMessages/MessageView.swift index d9be31d9..c71141a6 100644 --- a/SwiftMessages/MessageView.swift +++ b/SwiftMessages/MessageView.swift @@ -251,24 +251,72 @@ extension MessageView { */ public func configureTheme(_ theme: Theme, iconStyle: IconStyle = .default) { let iconImage = iconStyle.image(theme: theme) + let backgroundColor: UIColor + let foregroundColor: UIColor + let defaultBackgroundColor: UIColor + let defaultForegroundColor: UIColor switch theme { case .info: - let backgroundColor = UIColor(red: 225.0/255.0, green: 225.0/255.0, blue: 225.0/255.0, alpha: 1.0) - let foregroundColor = UIColor.darkText - configureTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconImage: iconImage) + defaultBackgroundColor = UIColor(red: 225.0/255.0, green: 225.0/255.0, blue: 225.0/255.0, alpha: 1.0) + defaultForegroundColor = UIColor.darkText case .success: - let backgroundColor = UIColor(red: 97.0/255.0, green: 161.0/255.0, blue: 23.0/255.0, alpha: 1.0) - let foregroundColor = UIColor.white - configureTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconImage: iconImage) + defaultBackgroundColor = UIColor(red: 97.0/255.0, green: 161.0/255.0, blue: 23.0/255.0, alpha: 1.0) + defaultForegroundColor = UIColor.white case .warning: - let backgroundColor = UIColor(red: 238.0/255.0, green: 189.0/255.0, blue: 34.0/255.0, alpha: 1.0) - let foregroundColor = UIColor.white - configureTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconImage: iconImage) + defaultBackgroundColor = UIColor(red: 246.0/255.0, green: 197.0/255.0, blue: 44.0/255.0, alpha: 1.0) + defaultForegroundColor = UIColor.white case .error: - let backgroundColor = UIColor(red: 249.0/255.0, green: 66.0/255.0, blue: 47.0/255.0, alpha: 1.0) - let foregroundColor = UIColor.white - configureTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconImage: iconImage) + defaultBackgroundColor = UIColor(red: 249.0/255.0, green: 66.0/255.0, blue: 47.0/255.0, alpha: 1.0) + defaultForegroundColor = UIColor.white } + if #available(iOS 13.0, *) { + switch theme { + case .info: + backgroundColor = UIColor { + switch $0.userInterfaceStyle { + case .dark, .unspecified: return UIColor(red: 125/255.0, green: 125/255.0, blue: 125/255.0, alpha: 1.0) + case .light: fallthrough + @unknown default: + return defaultBackgroundColor + } + } + foregroundColor = .label + case .success: + backgroundColor = UIColor { + switch $0.userInterfaceStyle { + case .dark, .unspecified: return UIColor(red: 55/255.0, green: 122/255.0, blue: 0/255.0, alpha: 1.0) + case .light: fallthrough + @unknown default: + return defaultBackgroundColor + } + } + foregroundColor = .white + case .warning: + backgroundColor = UIColor { + switch $0.userInterfaceStyle { + case .dark, .unspecified: return UIColor(red: 239/255.0, green: 184/255.0, blue: 10/255.0, alpha: 1.0) + case .light: fallthrough + @unknown default: + return defaultBackgroundColor + } + } + foregroundColor = .white + case .error: + backgroundColor = UIColor { + switch $0.userInterfaceStyle { + case .dark, .unspecified: return UIColor(red: 195/255.0, green: 12/255.0, blue: 12/255.0, alpha: 1.0) + case .light: fallthrough + @unknown default: + return defaultBackgroundColor + } + } + foregroundColor = .white + } + } else { + backgroundColor = defaultBackgroundColor + foregroundColor = defaultForegroundColor + } + configureTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconImage: iconImage) } /** From 7ef6043a9bd7e72308a27ef40645fd6df3942ba2 Mon Sep 17 00:00:00 2001 From: Timothy Moose Date: Tue, 13 Jul 2021 16:03:53 -0500 Subject: [PATCH 2/8] Remove availability check, which is not supported in iOS 15 --- SwiftMessages/SwiftMessages.Config+Extensions.swift | 2 +- SwiftMessages/SwiftMessages.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/SwiftMessages/SwiftMessages.Config+Extensions.swift b/SwiftMessages/SwiftMessages.Config+Extensions.swift index 9682b5b9..ac0475da 100644 --- a/SwiftMessages/SwiftMessages.Config+Extensions.swift +++ b/SwiftMessages/SwiftMessages.Config+Extensions.swift @@ -20,7 +20,7 @@ extension SwiftMessages.Config { @available (iOS 13.0, *) var windowScene: UIWindowScene? { switch presentationContext { - case .windowScene(let scene, _): return scene + case .windowScene(let scene, _): return scene as? UIWindowScene default: #if SWIFTMESSAGES_APP_EXTENSIONS return nil diff --git a/SwiftMessages/SwiftMessages.swift b/SwiftMessages/SwiftMessages.swift index dfae859c..882e7f0b 100644 --- a/SwiftMessages/SwiftMessages.swift +++ b/SwiftMessages/SwiftMessages.swift @@ -79,8 +79,7 @@ open class SwiftMessages { status bar. The only alternative is to set `Config.prefersStatusBarHidden = true` to hide it. */ - @available(iOS 13.0, *) - case windowScene(_: UIWindowScene, windowLevel: UIWindow.Level) + case windowScene(_: Any, windowLevel: UIWindow.Level) /** Displays the message view under navigation bars and tab bars if an From 3bb7f77bd149407e6efe7a13ac61649314fc2d62 Mon Sep 17 00:00:00 2001 From: Timothy Moose Date: Wed, 14 Jul 2021 14:08:10 -0500 Subject: [PATCH 3/8] Fix merge issues --- Demo/Demo/Base.lproj/Main.storyboard | 398 +++++++++++------ Demo/Demo/ViewControllersViewController.swift | 77 +++- SwiftMessages.podspec | 6 +- SwiftMessages.xcodeproj/project.pbxproj | 28 +- SwiftMessages/Animator.swift | 10 +- SwiftMessages/BaseView.swift | 131 +++--- SwiftMessages/CornerRoundingView.swift | 4 +- ...tomAnimation.swift => EdgeAnimation.swift} | 142 ++++-- SwiftMessages/Layout.swift | 104 +++++ SwiftMessages/MaskingView.swift | 404 +++++++++++++++++- SwiftMessages/MessageView.swift | 2 +- SwiftMessages/PhysicsAnimation.swift | 37 +- SwiftMessages/Presenter.swift | 4 +- SwiftMessages/Resources/CardView.xib | 25 +- SwiftMessages/Resources/CenteredView.xib | 27 +- SwiftMessages/Resources/TabView.xib | 25 +- SwiftMessages/SwiftMessagesSegue.swift | 228 ++++++---- .../UILayoutPriority+Extensions.swift | 26 ++ .../UIViewController+Extensions.swift | 6 +- 19 files changed, 1268 insertions(+), 416 deletions(-) rename SwiftMessages/{TopBottomAnimation.swift => EdgeAnimation.swift} (57%) create mode 100644 SwiftMessages/Layout.swift create mode 100644 SwiftMessages/UILayoutPriority+Extensions.swift diff --git a/Demo/Demo/Base.lproj/Main.storyboard b/Demo/Demo/Base.lproj/Main.storyboard index 08ab75d2..f8968f69 100644 --- a/Demo/Demo/Base.lproj/Main.storyboard +++ b/Demo/Demo/Base.lproj/Main.storyboard @@ -1,8 +1,9 @@ - + - + + @@ -30,7 +31,7 @@ - + @@ -47,7 +48,7 @@ @@ -81,7 +82,7 @@ @@ -116,7 +117,7 @@ @@ -151,7 +152,7 @@ @@ -185,7 +186,7 @@ @@ -223,7 +224,7 @@ - + @@ -641,7 +642,7 @@ @@ -655,7 +656,7 @@ @@ -674,7 +675,7 @@ @@ -688,7 +689,7 @@ @@ -810,115 +811,255 @@ - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - + + + + @@ -931,7 +1072,7 @@ - + @@ -951,7 +1092,7 @@ - + @@ -1049,18 +1190,19 @@ - + + @@ -1084,11 +1226,17 @@ - - - - + + + + + + + + + + diff --git a/Demo/Demo/ViewControllersViewController.swift b/Demo/Demo/ViewControllersViewController.swift index 6fb14eeb..733dc07d 100644 --- a/Demo/Demo/ViewControllersViewController.swift +++ b/Demo/Demo/ViewControllersViewController.swift @@ -19,27 +19,39 @@ class SwiftMessagesTopSegue: SwiftMessagesSegue { override public init(identifier: String?, source: UIViewController, destination: UIViewController) { super.init(identifier: identifier, source: source, destination: destination) configure(layout: .topMessage) + messageView.layout.size.height = .absolute(300) } } -class SwiftMessagesTopCardSegue: SwiftMessagesSegue { +class SwiftMessagesBottomSegue: SwiftMessagesSegue { override public init(identifier: String?, source: UIViewController, destination: UIViewController) { super.init(identifier: identifier, source: source, destination: destination) - configure(layout: .topCard) + configure(layout: .bottomMessage) + messageView.layout.size.height = .absolute(300) } } -class SwiftMessagesTopTabSegue: SwiftMessagesSegue { +class SwiftMessagesLeadingSegue: SwiftMessagesSegue { override public init(identifier: String?, source: UIViewController, destination: UIViewController) { super.init(identifier: identifier, source: source, destination: destination) - configure(layout: .topTab) + configure(layout: .leadingMessage) + messageView.layout.size.width = .absolute(300) } } -class SwiftMessagesBottomSegue: SwiftMessagesSegue { +class SwiftMessagesTrailingSegue: SwiftMessagesSegue { override public init(identifier: String?, source: UIViewController, destination: UIViewController) { super.init(identifier: identifier, source: source, destination: destination) - configure(layout: .bottomMessage) + configure(layout: .trailingMessage) + messageView.layout.size.width = .absolute(300) + } +} + +class SwiftMessagesTopCardSegue: SwiftMessagesSegue { + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + configure(layout: .topCard) + messageView.layout.size.height = .absolute(300) } } @@ -47,6 +59,31 @@ class SwiftMessagesBottomCardSegue: SwiftMessagesSegue { override public init(identifier: String?, source: UIViewController, destination: UIViewController) { super.init(identifier: identifier, source: source, destination: destination) configure(layout: .bottomCard) + messageView.layout.size.height = .absolute(300) + } +} + +class SwiftMessagesLeadingCardSegue: SwiftMessagesSegue { + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + configure(layout: .leadingCard) + messageView.layout.size.width = .absolute(300) + } +} + +class SwiftMessagesTrailingCardSegue: SwiftMessagesSegue { + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + configure(layout: .trailingCard) + messageView.layout.size.width = .absolute(300) + } +} + +class SwiftMessagesTopTabSegue: SwiftMessagesSegue { + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + configure(layout: .topTab) + messageView.layout.size.height = .absolute(300) } } @@ -54,6 +91,23 @@ class SwiftMessagesBottomTabSegue: SwiftMessagesSegue { override public init(identifier: String?, source: UIViewController, destination: UIViewController) { super.init(identifier: identifier, source: source, destination: destination) configure(layout: .bottomTab) + messageView.layout.size.height = .absolute(300) + } +} + +class SwiftMessagesLeadingTabSegue: SwiftMessagesSegue { + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + configure(layout: .leadingTab) + messageView.layout.size.width = .absolute(300) + } +} + +class SwiftMessagesTrailingTabSegue: SwiftMessagesSegue { + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + configure(layout: .trailingTab) + messageView.layout.size.width = .absolute(300) } } @@ -61,5 +115,16 @@ class SwiftMessagesCenteredSegue: SwiftMessagesSegue { override public init(identifier: String?, source: UIViewController, destination: UIViewController) { super.init(identifier: identifier, source: source, destination: destination) configure(layout: .centered) + messageView.layout.size.height = .absolute(300) + } +} + +class SwiftMessagesOffCenteredSegue: SwiftMessagesSegue { + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + configure(layout: .centered) + messageView.layout.insets.top = .absolute(0, from: .safeArea) + messageView.layout.center.x = .relative(0.33, in: .safeArea) + messageView.layout.size.height = .absolute(300) } } diff --git a/SwiftMessages.podspec b/SwiftMessages.podspec index a6c018fa..099fba91 100644 --- a/SwiftMessages.podspec +++ b/SwiftMessages.podspec @@ -1,14 +1,14 @@ Pod::Spec.new do |spec| spec.name = 'SwiftMessages' - spec.version = '9.0.3' + spec.version = '10.0.0' spec.license = { :type => 'MIT' } spec.homepage = 'https://github.com/SwiftKickMobile/SwiftMessages' spec.authors = { 'Timothy Moose' => 'tim@swiftkick.it' } spec.summary = 'A very flexible message bar for iOS written in Swift.' spec.source = {:git => 'https://github.com/SwiftKickMobile/SwiftMessages.git', :tag => spec.version} - spec.platform = :ios, '9.0' + spec.platform = :ios, '10.0' spec.swift_version = '5.0' - spec.ios.deployment_target = '9.0' + spec.ios.deployment_target = '10.0' spec.framework = 'UIKit' spec.requires_arc = true spec.default_subspec = 'App' diff --git a/SwiftMessages.xcodeproj/project.pbxproj b/SwiftMessages.xcodeproj/project.pbxproj index 5103fcdd..c9e779ae 100644 --- a/SwiftMessages.xcodeproj/project.pbxproj +++ b/SwiftMessages.xcodeproj/project.pbxproj @@ -51,9 +51,11 @@ 228DF5681FAD0806004F8A39 /* infoIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5471FAD0805004F8A39 /* infoIconSubtle.png */; }; 228DF5691FAD0806004F8A39 /* successIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5481FAD0805004F8A39 /* successIconLight.png */; }; 228DF56A1FAD0806004F8A39 /* infoIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5491FAD0805004F8A39 /* infoIconSubtle@3x.png */; }; + 2290944825D88A05002E8111 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2290944725D88A05002E8111 /* Layout.swift */; }; + 2290958125D9D407002E8111 /* UILayoutPriority+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2290958025D9D407002E8111 /* UILayoutPriority+Extensions.swift */; }; 2298C2051EE47DC900E2DDC1 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2041EE47DC900E2DDC1 /* Weak.swift */; }; 2298C2071EE480D000E2DDC1 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2061EE480D000E2DDC1 /* Animator.swift */; }; - 2298C2091EE486E300E2DDC1 /* TopBottomAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */; }; + 2298C2091EE486E300E2DDC1 /* EdgeAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2081EE486E300E2DDC1 /* EdgeAnimation.swift */; }; 229F778125FAB1E9008C2ACB /* UIWindow+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F778025FAB1E9008C2ACB /* UIWindow+Extensions.swift */; }; 22DFC9161EFF30F6001B1CA1 /* CenteredView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 22DFC9151EFF30F6001B1CA1 /* CenteredView.xib */; }; 22DFC9181F00674E001B1CA1 /* PhysicsPanHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */; }; @@ -138,10 +140,12 @@ 228DF5471FAD0805004F8A39 /* infoIconSubtle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = infoIconSubtle.png; path = Resources/infoIconSubtle.png; sourceTree = ""; }; 228DF5481FAD0805004F8A39 /* successIconLight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = successIconLight.png; path = Resources/successIconLight.png; sourceTree = ""; }; 228DF5491FAD0805004F8A39 /* infoIconSubtle@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "infoIconSubtle@3x.png"; path = "Resources/infoIconSubtle@3x.png"; sourceTree = ""; }; + 2290944725D88A05002E8111 /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = ""; }; + 2290958025D9D407002E8111 /* UILayoutPriority+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILayoutPriority+Extensions.swift"; sourceTree = ""; }; 2298C2041EE47DC900E2DDC1 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; 2298C2061EE480D000E2DDC1 /* Animator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Animator.swift; sourceTree = ""; }; - 2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopBottomAnimation.swift; sourceTree = ""; }; - 229F778025FAB1E9008C2ACB /* UIWindow+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+Extensions.swift"; sourceTree = ""; }; + 2298C2081EE486E300E2DDC1 /* EdgeAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EdgeAnimation.swift; sourceTree = ""; }; + 229F778025FAB1E9008C2ACB /* UIWindow+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+Extensions.swift"; sourceTree = ""; }; 22A2EA6E24EC6CFA00BB2540 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 22DFC9151EFF30F6001B1CA1 /* CenteredView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = CenteredView.xib; path = Resources/CenteredView.xib; sourceTree = ""; }; 22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhysicsPanHandler.swift; sourceTree = ""; }; @@ -211,7 +215,8 @@ children = ( 220655111FAF82B600F4E00F /* MarginAdjustable+Extensions.swift */, 22774B9F20B5EF2A00813732 /* UIEdgeInsets+Extensions.swift */, - 229F778025FAB1E9008C2ACB /* UIWindow+Extensions.swift */, + 2290958025D9D407002E8111 /* UILayoutPriority+Extensions.swift */, + 229F778025FAB1E9008C2ACB /* UIWindow+Extensions.swift */, ); name = Extensions; sourceTree = ""; @@ -219,7 +224,7 @@ 2244656C1EF1D62700C50413 /* Animations */ = { isa = PBXGroup; children = ( - 2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */, + 2298C2081EE486E300E2DDC1 /* EdgeAnimation.swift */, 2270044A1FAFA6DD0045DDC3 /* PhysicsAnimation.swift */, 22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */, ); @@ -297,6 +302,7 @@ 8644955C1D4FAF7C0056EB2A /* WindowViewController.swift */, 867BED201D622793005212E3 /* BackgroundViewable.swift */, 864495551D4F7C390056EB2A /* Identifiable.swift */, + 2290944725D88A05002E8111 /* Layout.swift */, 86AAF81D1D5549680031EE32 /* MarginAdjustable.swift */, 22E307FE1E74C5B100E35893 /* AccessibleMessage.swift */, 86AAF82A1D580DD70031EE32 /* Error.swift */, @@ -533,6 +539,7 @@ 86BBA8FC1D5E03F100FE8F16 /* MessageView.swift in Sources */, 86BBA9061D5E040C00FE8F16 /* Identifiable.swift in Sources */, 22F27951210CE25900273E7F /* CornerRoundingView.swift in Sources */, + 2290944825D88A05002E8111 /* Layout.swift in Sources */, 86BBA9011D5E040600FE8F16 /* PassthroughWindow.swift in Sources */, 2298C2071EE480D000E2DDC1 /* Animator.swift in Sources */, 86BBA9031D5E040600FE8F16 /* UIViewController+Extensions.swift in Sources */, @@ -553,11 +560,12 @@ 86BBA8FF1D5E040600FE8F16 /* Presenter.swift in Sources */, 86BBA9051D5E040C00FE8F16 /* Theme.swift in Sources */, 86BBA9081D5E040C00FE8F16 /* Error.swift in Sources */, - 2298C2091EE486E300E2DDC1 /* TopBottomAnimation.swift in Sources */, + 2298C2091EE486E300E2DDC1 /* EdgeAnimation.swift in Sources */, 86589D471D64B6E40041676C /* BaseView.swift in Sources */, 225304622290C76E00A03ACF /* NSLayoutConstraint+Extensions.swift in Sources */, 86BBA9071D5E040C00FE8F16 /* MarginAdjustable.swift in Sources */, 867BED211D622793005212E3 /* BackgroundViewable.swift in Sources */, + 2290958125D9D407002E8111 /* UILayoutPriority+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -629,7 +637,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -681,7 +689,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -703,7 +711,7 @@ GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES; INFOPLIST_FILE = SwiftMessages/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -731,7 +739,7 @@ GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES; INFOPLIST_FILE = SwiftMessages/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/SwiftMessages/Animator.swift b/SwiftMessages/Animator.swift index 82074068..a26ea611 100644 --- a/SwiftMessages/Animator.swift +++ b/SwiftMessages/Animator.swift @@ -44,13 +44,17 @@ public struct SafeZoneConflicts: OptionSet { } public class AnimationContext { - public let messageView: UIView - public let containerView: UIView + public let containerView: UIView & LayoutInstalling public let safeZoneConflicts: SafeZoneConflicts public let interactiveHide: Bool - init(messageView: UIView, containerView: UIView, safeZoneConflicts: SafeZoneConflicts, interactiveHide: Bool) { + internal init( + messageView: UIView, + containerView: UIView & LayoutInstalling, + safeZoneConflicts: SafeZoneConflicts, + interactiveHide: Bool + ) { self.messageView = messageView self.containerView = containerView self.safeZoneConflicts = safeZoneConflicts diff --git a/SwiftMessages/BaseView.swift b/SwiftMessages/BaseView.swift index 9480b87d..cdde9008 100644 --- a/SwiftMessages/BaseView.swift +++ b/SwiftMessages/BaseView.swift @@ -14,7 +14,7 @@ import UIKit of the optional SwiftMessages protocols and provides some convenience functions and a configurable tap handler. Message views do not need to inherit from `BaseVew`. */ -open class BaseView: UIView, BackgroundViewable, MarginAdjustable { +open class BaseView: UIView, BackgroundViewable, MarginAdjustable, LayoutDefining { /* MARK: - IB outlets @@ -63,6 +63,7 @@ open class BaseView: UIView, BackgroundViewable, MarginAdjustable { */ /** + TODO SIZE - update documentation A convenience function for installing a content view as a subview of `backgroundView` and pinning the edges to `backgroundView` with the specified `insets`. @@ -82,6 +83,7 @@ open class BaseView: UIView, BackgroundViewable, MarginAdjustable { } /** + TODO SIZE - update documentation - insets removed A convenience function for installing a background view and pinning to the layout margins. This is useful for creating programatic layouts where the background view needs to be inset from the message view's edges (like a card-style layout). @@ -90,63 +92,66 @@ open class BaseView: UIView, BackgroundViewable, MarginAdjustable { assigned to the `backgroundView` property. - Parameter insets: The amount to inset the content view from the margins. Default is zero inset. */ - open func installBackgroundView(_ backgroundView: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) { + open func installBackgroundView(_ backgroundView: UIView) { backgroundView.translatesAutoresizingMaskIntoConstraints = false if backgroundView != self { backgroundView.removeFromSuperview() } addSubview(backgroundView) self.backgroundView = backgroundView - backgroundView.centerXAnchor.constraint(equalTo: centerXAnchor).with(priority: UILayoutPriority(rawValue: 950)).isActive = true - backgroundView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: insets.top).with(priority: UILayoutPriority(rawValue: 900)).isActive = true - backgroundView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor, constant: -insets.bottom).with(priority: UILayoutPriority(rawValue: 900)).isActive = true - backgroundView.heightAnchor.constraint(equalToConstant: 350).with(priority: UILayoutPriority(rawValue: 200)).isActive = true - layoutConstraints = [ - backgroundView.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)), - backgroundView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)), - ] - regularWidthLayoutConstraints = [ - backgroundView.leftAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)), - backgroundView.rightAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)), - backgroundView.widthAnchor.constraint(lessThanOrEqualToConstant: 500).with(priority: UILayoutPriority(rawValue: 950)), - backgroundView.widthAnchor.constraint(equalToConstant: 500).with(priority: UILayoutPriority(rawValue: 200)), - ] + NSLayoutConstraint.activate([ + backgroundView.centerXAnchor.constraint(equalTo: centerXAnchor) + .with(priority: .belowMessageSizeable), + backgroundView.topAnchor.constraint(equalTo: topAnchor) + .with(priority: .belowMessageSizeable), + backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor) + .with(priority: .belowMessageSizeable), + backgroundView.heightAnchor.constraint(equalToConstant: 350) + .with(priority: UILayoutPriority(rawValue: 200)), + backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor) + .with(priority: .belowMessageSizeable), + backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor) + .with(priority: .belowMessageSizeable), + ]) installTapRecognizer() } - /** - A convenience function for installing a background view and pinning to the horizontal - layout margins and to the vertical edges. This is useful for creating programatic layouts where - the background view needs to be inset from the message view's horizontal edges (like a tab-style layout). - - - Parameter backgroundView: The view to be installed as a subview and - assigned to the `backgroundView` property. - - Parameter insets: The amount to inset the content view from the horizontal margins and vertical edges. - Default is zero inset. - */ - open func installBackgroundVerticalView(_ backgroundView: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) { - backgroundView.translatesAutoresizingMaskIntoConstraints = false - if backgroundView != self { - backgroundView.removeFromSuperview() - } - addSubview(backgroundView) - self.backgroundView = backgroundView - backgroundView.centerXAnchor.constraint(equalTo: centerXAnchor).with(priority: UILayoutPriority(rawValue: 950)).isActive = true - backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: insets.top).with(priority: UILayoutPriority(rawValue: 1000)).isActive = true - backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom).with(priority: UILayoutPriority(rawValue: 1000)).isActive = true - backgroundView.heightAnchor.constraint(equalToConstant: 350).with(priority: UILayoutPriority(rawValue: 200)).isActive = true - layoutConstraints = [ - backgroundView.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)), - backgroundView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)), - ] - regularWidthLayoutConstraints = [ - backgroundView.leftAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)), - backgroundView.rightAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)), - backgroundView.widthAnchor.constraint(lessThanOrEqualToConstant: 500).with(priority: UILayoutPriority(rawValue: 950)), - backgroundView.widthAnchor.constraint(equalToConstant: 500).with(priority: UILayoutPriority(rawValue: 200)), - ] - installTapRecognizer() - } +// /** +// A convenience function for installing a background view and pinning to the horizontal +// layout margins and to the vertical edges. This is useful for creating programatic layouts where +// the background view needs to be inset from the message view's horizontal edges (like a tab-style layout). +// +// - Parameter backgroundView: The view to be installed as a subview and +// assigned to the `backgroundView` property. +// - Parameter insets: The amount to inset the content view from the horizontal margins and vertical edges. +// Default is zero inset. +// */ +// open func installBackgroundVerticalView(_ backgroundView: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) { +// backgroundView.translatesAutoresizingMaskIntoConstraints = false +// if backgroundView != self { +// backgroundView.removeFromSuperview() +// } +// addSubview(backgroundView) +// self.backgroundView = backgroundView +// NSLayoutConstraint.activate([ +// backgroundView.centerXAnchor.constraint(equalTo: centerXAnchor) +// .with(priority: .belowMessageSizeable), +// backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: insets.top) +// .with(priority: .required), +// backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom) +// .with(priority: .required), +// backgroundView.heightAnchor.constraint(equalToConstant: 350) +// .with(priority: UILayoutPriority(rawValue: 200)), +// backgroundView.leftAnchor.constraint( +// equalTo: layoutMarginsGuide.leftAnchor, constant: insets.left +// ).with(priority: .belowMessageSizeable), +// backgroundView.rightAnchor.constraint( +// equalTo: layoutMarginsGuide.rightAnchor, +// constant: -insets.right +// ).with(priority: .belowMessageSizeable), +// ]) +// installTapRecognizer() +// } /* MARK: - Tap handler @@ -190,6 +195,11 @@ open class BaseView: UIView, BackgroundViewable, MarginAdjustable { return super.point(inside: point, with: event) } + // MARK: - MessageSizeable + + /// Configure the view's layout + public var layout = Layout() + /* MARK: - MarginAdjustable @@ -257,29 +267,6 @@ open class BaseView: UIView, BackgroundViewable, MarginAdjustable { } private var backgroundHeightConstraint: NSLayoutConstraint? - - /* - Mark: - Layout - */ - - open override func updateConstraints() { - super.updateConstraints() - let on: [NSLayoutConstraint] - let off: [NSLayoutConstraint] - switch traitCollection.horizontalSizeClass { - case .regular: - on = regularWidthLayoutConstraints - off = layoutConstraints - default: - on = layoutConstraints - off = regularWidthLayoutConstraints - } - on.forEach { $0.isActive = true } - off.forEach { $0.isActive = false } - } - - private var layoutConstraints: [NSLayoutConstraint] = [] - private var regularWidthLayoutConstraints: [NSLayoutConstraint] = [] } /* diff --git a/SwiftMessages/CornerRoundingView.swift b/SwiftMessages/CornerRoundingView.swift index 398b731c..7db484fb 100644 --- a/SwiftMessages/CornerRoundingView.swift +++ b/SwiftMessages/CornerRoundingView.swift @@ -24,12 +24,12 @@ open class CornerRoundingView: UIView { /// rounded. For example, the layout in TabView.xib rounds the bottom corners /// when displayed from the top and the top corners when displayed from the bottom. /// When this property is `true`, the `roundedCorners` property will be overwritten - /// by relevant animators (e.g. `TopBottomAnimation`). + /// by relevant animators (e.g. `EdgeAnimation`). @IBInspectable open var roundsLeadingCorners: Bool = false /// Specifies which corners should be rounded. When `roundsLeadingCorners = true`, relevant - /// relevant animators (e.g. `TopBottomAnimation`) will overwrite the value of this property. + /// relevant animators (e.g. `EdgeAnimation`) will overwrite the value of this property. open var roundedCorners: UIRectCorner = [.allCorners] { didSet { updateMaskPath() diff --git a/SwiftMessages/TopBottomAnimation.swift b/SwiftMessages/EdgeAnimation.swift similarity index 57% rename from SwiftMessages/TopBottomAnimation.swift rename to SwiftMessages/EdgeAnimation.swift index 21b0f271..218ecda0 100644 --- a/SwiftMessages/TopBottomAnimation.swift +++ b/SwiftMessages/EdgeAnimation.swift @@ -1,5 +1,5 @@ // -// TopBottomAnimation.swift +// EdgeAnimation.swift // SwiftMessages // // Created by Timothy Moose on 6/4/17. @@ -8,20 +8,25 @@ import UIKit -public class TopBottomAnimation: NSObject, Animator { +@available(*, deprecated, message: "Class renamed to `EdgeAnimation` to reflect new ability to do leading and trailing animations.") +public typealias TopBottomAnimation = EdgeAnimation + +public class EdgeAnimation: NSObject, Animator { public enum Style { case top case bottom + case leading + case trailing } public weak var delegate: AnimationDelegate? public let style: Style - public var showDuration: TimeInterval = 0.4 + public var showDuration: TimeInterval = 0.35 - public var hideDuration: TimeInterval = 0.2 + public var hideDuration: TimeInterval = 0.25 public var springDamping: CGFloat = 0.8 @@ -60,21 +65,25 @@ public class TopBottomAnimation: NSObject, Animator { NotificationCenter.default.removeObserver(self) let view = context.messageView self.context = context - UIView.animate(withDuration: hideDuration, delay: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: { + UIView.animate(withDuration: hideDuration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.beginFromCurrentState]) { switch self.style { case .top: view.transform = CGAffineTransform(translationX: 0, y: -view.frame.height) case .bottom: view.transform = CGAffineTransform(translationX: 0, y: view.frame.maxY + view.frame.height) + case .leading: // TODO SIZE do proper leading and trailing + view.transform = CGAffineTransform(translationX: -view.frame.width, y: 0) + case .trailing: + view.transform = CGAffineTransform(translationX: view.frame.maxX + view.frame.width, y: 0) } - }, completion: { completed in + } completion: { completed in #if SWIFTMESSAGES_APP_EXTENSIONS completion(completed) #else // Fix #131 by always completing if application isn't active. completion(completed || UIApplication.shared.applicationState != .active) #endif - }) + } } func install(context: AnimationContext) { @@ -86,26 +95,66 @@ public class TopBottomAnimation: NSObject, Animator { if let adjustable = context.messageView as? MarginAdjustable { bounceOffset = adjustable.bounceAnimationOffset } - view.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(view) - view.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true - view.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true + if let layoutDefiningView = view as? LayoutDefining & UIView { + container.install(layoutDefiningView: layoutDefiningView) + } else { + view.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(view) + } + // Horizontal constraints + do { + } switch style { case .top: - view.topAnchor.constraint(equalTo: container.topAnchor, constant: -bounceOffset).with(priority: UILayoutPriority(200)).isActive = true + NSLayoutConstraint.activate([ + view.leadingAnchor.constraint(equalTo: container.leadingAnchor) + .with(priority: .belowMessageSizeable - 1), + view.centerXAnchor.constraint(equalTo: container.centerXAnchor) + .with(priority: .belowMessageSizeable), + view.topAnchor.constraint(equalTo: container.topAnchor, constant: -bounceOffset) + .with(priority: .belowMessageSizeable) + ]) case .bottom: - view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: bounceOffset).with(priority: UILayoutPriority(200)).isActive = true + NSLayoutConstraint.activate([ + view.leadingAnchor.constraint(equalTo: container.leadingAnchor) + .with(priority: .belowMessageSizeable - 1), + view.centerXAnchor.constraint(equalTo: container.centerXAnchor) + .with(priority: .belowMessageSizeable), + view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: bounceOffset) + .with(priority: .belowMessageSizeable) + ]) + case .leading: + NSLayoutConstraint.activate([ + view.topAnchor.constraint(equalTo: container.topAnchor) + .with(priority: .belowMessageSizeable - 1), + view.centerYAnchor.constraint(equalTo: container.centerYAnchor) + .with(priority: .belowMessageSizeable), + view.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: -bounceOffset) + .with(priority: .belowMessageSizeable) + ]) + case .trailing: + NSLayoutConstraint.activate([ + view.topAnchor.constraint(equalTo: container.topAnchor) + .with(priority: .belowMessageSizeable - 1), + view.centerYAnchor.constraint(equalTo: container.centerYAnchor) + .with(priority: .belowMessageSizeable), + view.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: bounceOffset) + .with(priority: .belowMessageSizeable) + ]) } // Important to layout now in order to get the right safe area insets container.layoutIfNeeded() adjustMargins() container.layoutIfNeeded() - let animationDistance = view.frame.height switch style { case .top: - view.transform = CGAffineTransform(translationX: 0, y: -animationDistance) + view.transform = CGAffineTransform(translationX: 0, y: -view.frame.height) case .bottom: - view.transform = CGAffineTransform(translationX: 0, y: animationDistance) + view.transform = CGAffineTransform(translationX: 0, y: view.frame.height) + case .leading: + view.transform = CGAffineTransform(translationX: -view.frame.width, y: 0) + case .trailing: + view.transform = CGAffineTransform(translationX: view.frame.width, y: 0) } if context.interactiveHide { if let view = view as? BackgroundViewable { @@ -122,6 +171,10 @@ public class TopBottomAnimation: NSObject, Animator { cornerRoundingView.roundedCorners = [.bottomLeft, .bottomRight] case .bottom: cornerRoundingView.roundedCorners = [.topLeft, .topRight] + case .leading: + cornerRoundingView.roundedCorners = [.topRight, .bottomRight] + case .trailing: + cornerRoundingView.roundedCorners = [.topLeft, .bottomLeft] } } } @@ -139,6 +192,10 @@ public class TopBottomAnimation: NSObject, Animator { layoutMargins.top += bounceOffset case .bottom: layoutMargins.bottom += bounceOffset + case .leading: + layoutMargins.left += bounceOffset + case .trailing: + layoutMargins.right += bounceOffset } adjustable.layoutMargins = layoutMargins } @@ -174,21 +231,39 @@ public class TopBottomAnimation: NSObject, Animator { fileprivate var rubberBanding = false fileprivate var closeSpeed: CGFloat = 0.0 fileprivate var closePercent: CGFloat = 0.0 - fileprivate var panTranslationY: CGFloat = 0.0 + fileprivate var panTranslation: CGFloat = 0.0 @objc func pan(_ pan: UIPanGestureRecognizer) { switch pan.state { case .changed: guard let view = messageView else { return } - let height = view.bounds.height - bounceOffset - if height <= 0 { return } + let length: CGFloat + switch style { + case .top, .bottom: + length = view.bounds.height - bounceOffset + case .leading, .trailing: + length = view.bounds.width - bounceOffset + } + if length <= 0 { return } var velocity = pan.velocity(in: view) var translation = pan.translation(in: view) - if case .top = style { + switch style { + case .top: velocity.y *= -1.0 translation.y *= -1.0 + case .leading: + velocity.x *= -1.0 + translation.x *= -1.0 + case .bottom, .trailing: + break + } + var translationAmount: CGFloat + switch style { + case .top, .bottom: + translationAmount = translation.y >= 0 ? translation.y : -pow(abs(translation.y), 0.7) + case .leading, .trailing: + translationAmount = translation.x >= 0 ? translation.x : -pow(abs(translation.x), 0.7) } - var translationAmount = translation.y >= 0 ? translation.y : -pow(abs(translation.y), 0.7) if !closing { // Turn on rubber banding if background view is inset from message view. if let background = (messageView as? BackgroundViewable)?.backgroundView, background != view { @@ -197,6 +272,10 @@ public class TopBottomAnimation: NSObject, Animator { rubberBanding = background.frame.minY > 0 case .bottom: rubberBanding = background.frame.maxY < view.bounds.height + case .leading: + rubberBanding = background.frame.minX > 0 + case .trailing: + rubberBanding = background.frame.maxX < view.bounds.width } } if !rubberBanding && translationAmount < 0 { return } @@ -209,19 +288,30 @@ public class TopBottomAnimation: NSObject, Animator { view.transform = CGAffineTransform(translationX: 0, y: -translationAmount) case .bottom: view.transform = CGAffineTransform(translationX: 0, y: translationAmount) + case .leading: + view.transform = CGAffineTransform(translationX: -translationAmount, y: 0) + case .trailing: + view.transform = CGAffineTransform(translationX: translationAmount, y: 0) + } + switch style { + case .top, .bottom: + closeSpeed = velocity.y + closePercent = translation.y / length + panTranslation = translation.y + case .leading, .trailing: + closeSpeed = velocity.x + closePercent = translation.x / length + panTranslation = translation.x } - closeSpeed = velocity.y - closePercent = translation.y / height - panTranslationY = translation.y case .ended, .cancelled: - if closeSpeed > closeSpeedThreshold || closePercent > closePercentThreshold || panTranslationY > closeAbsoluteThreshold { + if closeSpeed > closeSpeedThreshold || closePercent > closePercentThreshold || panTranslation > closeAbsoluteThreshold { delegate?.hide(animator: self) } else { closing = false rubberBanding = false closeSpeed = 0.0 closePercent = 0.0 - panTranslationY = 0.0 + panTranslation = 0.0 showAnimation(completion: { (completed) in self.delegate?.panEnded(animator: self) }) diff --git a/SwiftMessages/Layout.swift b/SwiftMessages/Layout.swift new file mode 100644 index 00000000..09c2c16f --- /dev/null +++ b/SwiftMessages/Layout.swift @@ -0,0 +1,104 @@ +// +// BoundaryInsets.swift +// SwiftMessages +// +// Created by Timothy Moose on 2/13/21. +// Copyright © 2021 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +public protocol LayoutDefining { + var layout: Layout { get } +} + +public protocol LayoutInstalling { + func install(layoutDefiningView: LayoutDefining & UIView) +} + +public struct Layout { + + public var size = Size() + public var insets = Insets() + public var center = Center() + public var min = Layout() + public var max = Layout() + + public init() {} + + public enum Boundary { + case superview + case safeArea + case margin + } + + public struct Size { + + public enum Dimension { + case absolute(CGFloat) + case relative(CGFloat, to: Boundary) + case absoluteInsets(CGFloat, from: Boundary) + } + + public var width: Dimension? + public var height: Dimension? + } + + public struct Insets { + + public enum Dimension { + case absolute(CGFloat, from: Boundary) + case relative(CGFloat, from: Boundary) + } + + public var top: Dimension? + public var bottom: Dimension? + public var leading: Dimension? + public var trailing: Dimension? + } + + public struct Center { + + public enum Dimension { + case absolute(CGFloat, in: Boundary) + case relative(CGFloat, in: Boundary) + } + + public var x: Dimension? + public var y: Dimension? + } + + public struct Layout { + public var size = Size() + public var insets = Insets() + public var center = Center() + } +} + +extension Layout.Insets.Dimension { + var boundary: Layout.Boundary { + switch self { + case .absolute(_, let boundary): return boundary + case .relative(_, let boundary): return boundary + } + } +} + +extension Layout.Size.Dimension { + var boundary: Layout.Boundary? { + switch self { + case .absolute(_): return nil + case .relative(_, let boundary): return boundary + case .absoluteInsets(_, let boundary): return boundary + } + } +} + +extension Layout.Center.Dimension { + var boundary: Layout.Boundary { + switch self { + case .absolute(_, let boundary): return boundary + case .relative(_, let boundary): return boundary + } + } +} diff --git a/SwiftMessages/MaskingView.swift b/SwiftMessages/MaskingView.swift index 0ce4c428..08c31d20 100644 --- a/SwiftMessages/MaskingView.swift +++ b/SwiftMessages/MaskingView.swift @@ -8,8 +8,31 @@ import UIKit +// TODO SIZE - need a version of this logic to limit views to 500pt on regular size class by default. +// regularWidthLayoutConstraints = [ +// backgroundView.leftAnchor.constraint( +// greaterThanOrEqualTo: layoutMarginsGuide.leftAnchor, +// constant: insets.left +// ).with(priority: .belowMessageSizeable), +// backgroundView.rightAnchor.constraint( +// lessThanOrEqualTo: layoutMarginsGuide.rightAnchor, +// constant: -insets.right +// ).with(priority: .belowMessageSizeable), +// backgroundView.widthAnchor.constraint(lessThanOrEqualToConstant: 500) +// .with(priority: .belowMessageSizeable), +// backgroundView.widthAnchor.constraint(equalToConstant: 500) +// .with(priority: UILayoutPriority(rawValue: 200)), +// ] -class MaskingView: PassthroughView { +class MaskingView: PassthroughView, LayoutInstalling { + + func install(layoutDefiningView: UIView & LayoutDefining) { + self.layoutDefiningView?.removeFromSuperview() + self.layoutDefiningView = layoutDefiningView + layoutDefiningView.translatesAutoresizingMaskIntoConstraints = false + addSubview(layoutDefiningView) + setNeedsUpdateConstraints() + } func install(keyboardTrackingView: KeyboardTrackingView) { self.keyboardTrackingView = keyboardTrackingView @@ -50,15 +73,30 @@ class MaskingView: PassthroughView { init() { super.init(frame: CGRect.zero) - clipsToBounds = true + postInit() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) + postInit() + } + + private func postInit() { clipsToBounds = true + addLayoutGuide(messageInsetsGuide) + } + + override var bounds: CGRect { + didSet { + guard bounds != oldValue else { return } + setNeedsUpdateConstraints() + } } private var keyboardTrackingView: KeyboardTrackingView? + private var cachedConstraints: [NSLayoutConstraint] = [] + private let messageInsetsGuide = UILayoutGuide() + private var layoutDefiningView: (LayoutDefining & UIView)? override func addSubview(_ view: UIView) { super.addSubview(view) @@ -67,4 +105,366 @@ class MaskingView: PassthroughView { view != backgroundView else { return } keyboardTrackingView.topAnchor.constraint(greaterThanOrEqualTo: view.bottomAnchor).with(priority: UILayoutPriority(250)).isActive = true } + + override func updateConstraints() { + super.updateConstraints() + NSLayoutConstraint.deactivate(cachedConstraints) + cachedConstraints = [] + if let layoutDefiningView = layoutDefiningView { + let layout = layoutDefiningView.layout + add(insets: layout.insets, relation: .equal) + add(insets: layout.min.insets, relation: .min) + add(insets: layout.max.insets, relation: .max) + add(layoutDefiningView: layoutDefiningView) + } + NSLayoutConstraint.activate(cachedConstraints) + } + + private enum ConstraintRelation { + case equal + case min + case max + } + + private func add(insets: Layout.Insets, relation: ConstraintRelation) { + if let top = insets.top { add(top: top, relation: relation) } + if let bottom = insets.bottom { add(bottom: bottom, relation: relation) } + if let leading = insets.leading { add(leading: leading, relation: relation) } + if let trailing = insets.trailing { add(trailing: trailing, relation: relation) } + } + + private func add(top: Layout.Insets.Dimension, relation: ConstraintRelation) { + let length: CGFloat + switch top { + case .absolute(let dimension, _): + length = dimension + case .relative(let percentage, _): + length = bounds.height * percentage + } + let otherAnchor: NSLayoutYAxisAnchor + switch top.boundary { + case .superview: otherAnchor = topAnchor + case .margin: otherAnchor = layoutMarginsGuide.topAnchor + case .safeArea: + if #available(iOS 11.0, *) { + otherAnchor = safeAreaLayoutGuide.topAnchor + } else { + otherAnchor = layoutMarginsGuide.topAnchor + } + } + add( + anchor: messageInsetsGuide.topAnchor, + otherAnchor: otherAnchor, + constant: length, + relation: relation + ) + } + + private func add(bottom: Layout.Insets.Dimension, relation: ConstraintRelation) { + let length: CGFloat + switch bottom { + case .absolute(let dimension, _): + length = dimension + case .relative(let percentage, _): + length = bounds.height * percentage + } + let otherAnchor: NSLayoutYAxisAnchor + switch bottom.boundary { + case .superview: otherAnchor = bottomAnchor + case .margin: otherAnchor = layoutMarginsGuide.bottomAnchor + case .safeArea: + if #available(iOS 11.0, *) { + otherAnchor = safeAreaLayoutGuide.bottomAnchor + } else { + otherAnchor = layoutMarginsGuide.bottomAnchor + } + } + add( + anchor: otherAnchor, + otherAnchor: messageInsetsGuide.bottomAnchor, + constant: length, + relation: relation + ) + } + + private func add(leading: Layout.Insets.Dimension, relation: ConstraintRelation) { + let length: CGFloat + switch leading { + case .absolute(let dimension, _): + length = dimension + case .relative(let percentage, _): + length = bounds.width * percentage + } + let otherAnchor: NSLayoutXAxisAnchor + switch leading.boundary { + case .superview: otherAnchor = leadingAnchor + case .margin: otherAnchor = layoutMarginsGuide.leadingAnchor + case .safeArea: + if #available(iOS 11.0, *) { + otherAnchor = safeAreaLayoutGuide.leadingAnchor + } else { + otherAnchor = layoutMarginsGuide.leadingAnchor + } + } + add( + anchor: messageInsetsGuide.leadingAnchor, + otherAnchor: otherAnchor, + constant: length, + relation: relation + ) + } + + private func add(trailing: Layout.Insets.Dimension, relation: ConstraintRelation) { + let length: CGFloat + switch trailing { + case .absolute(let dimension, _): + length = dimension + case .relative(let percentage, _): + length = bounds.width * percentage + } + let otherAnchor: NSLayoutXAxisAnchor + switch trailing.boundary { + case .superview: otherAnchor = trailingAnchor + case .margin: otherAnchor = layoutMarginsGuide.trailingAnchor + case .safeArea: + if #available(iOS 11.0, *) { + otherAnchor = safeAreaLayoutGuide.trailingAnchor + } else { + otherAnchor = layoutMarginsGuide.trailingAnchor + } + } + add( + anchor: otherAnchor, + otherAnchor: messageInsetsGuide.trailingAnchor, + constant: length, + relation: relation + ) + } + + private func add( + anchor: NSLayoutXAxisAnchor, + otherAnchor: NSLayoutXAxisAnchor, + constant: CGFloat, + relation: ConstraintRelation + ) { + let constraint: NSLayoutConstraint + switch relation { + case .equal: + constraint = anchor + .constraint(equalTo: otherAnchor, constant: constant) + .with(priority: .messageInsets) + case .min: + constraint = anchor + .constraint(greaterThanOrEqualTo: otherAnchor, constant: constant) + .with(priority: .messageInsetsBounds) + case .max: + constraint = anchor + .constraint(lessThanOrEqualTo: otherAnchor, constant: constant) + .with(priority: .messageInsetsBounds) + } + cachedConstraints.append(constraint) + } + + private func add( + anchor: NSLayoutYAxisAnchor, + otherAnchor: NSLayoutYAxisAnchor, + constant: CGFloat, + relation: ConstraintRelation + ) { + let constraint: NSLayoutConstraint + switch relation { + case .equal: + constraint = anchor + .constraint(equalTo: otherAnchor, constant: constant) + .with(priority: .messageInsets) + case .min: + constraint = anchor + .constraint(greaterThanOrEqualTo: otherAnchor, constant: constant) + .with(priority: .messageInsetsBounds) + case .max: + constraint = anchor + .constraint(lessThanOrEqualTo: otherAnchor, constant: constant) + .with(priority: .messageInsetsBounds) + } + cachedConstraints.append(constraint) + } + + private func add(layoutDefiningView view: LayoutDefining & UIView) { + cachedConstraints += [ + messageInsetsGuide.topAnchor.constraint(equalTo: view.topAnchor), + messageInsetsGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor), + messageInsetsGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor), + messageInsetsGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ] + add(layoutDefiningView: view, size: view.layout.size, relation: .equal) + add(layoutDefiningView: view, size: view.layout.min.size, relation: .min) + add(layoutDefiningView: view, size: view.layout.max.size, relation: .max) + add(layoutDefiningView: view, center: view.layout.center, relation: .equal) + add(layoutDefiningView: view, center: view.layout.min.center, relation: .min) + add(layoutDefiningView: view, center: view.layout.max.center, relation: .max) + } + + private func add( + layoutDefiningView view: LayoutDefining & UIView, + size: Layout.Size, + relation: ConstraintRelation + ) { + if let width = size.width { + let length = self.length(for: width) { $0.width } + let constraint: NSLayoutConstraint + switch relation { + case .equal: + constraint = view.widthAnchor.constraint(equalToConstant: length) + .with(priority: .messageSize) + case .min: + constraint = view.widthAnchor.constraint(greaterThanOrEqualToConstant: length) + .with(priority: .messageSizeBounds) + case .max: + constraint = view.widthAnchor.constraint(lessThanOrEqualToConstant: length) + .with(priority: .messageSizeBounds) + } + cachedConstraints.append(constraint) + } + if let height = size.height { + let length = self.length(for: height) { $0.height } + let constraint: NSLayoutConstraint + switch relation { + case .equal: + constraint = view.heightAnchor.constraint(equalToConstant: length) + .with(priority: .messageSize) + case .min: + constraint = view.heightAnchor.constraint(greaterThanOrEqualToConstant: length) + .with(priority: .messageSizeBounds) + case .max: + constraint = view.heightAnchor.constraint(lessThanOrEqualToConstant: length) + .with(priority: .messageSizeBounds) + } + cachedConstraints.append(constraint) + } + } + + + private func add( + layoutDefiningView view: LayoutDefining & UIView, + center: Layout.Center, + relation: ConstraintRelation + ) { + if let x = center.x { + let length = self.length(for: x) { ($0.minX, $0.maxX) } + let otherAnchor: NSLayoutXAxisAnchor + switch x.boundary { + case .superview: otherAnchor = leadingAnchor + case .margin: otherAnchor = layoutMarginsGuide.leadingAnchor + case .safeArea: + if #available(iOS 11.0, *) { + otherAnchor = safeAreaLayoutGuide.leadingAnchor + } else { + otherAnchor = layoutMarginsGuide.leadingAnchor + } + } + let constraint: NSLayoutConstraint + switch relation { + case .equal: + constraint = view.centerXAnchor.constraint(equalTo: otherAnchor, constant: length) + .with(priority: .messageCenter) + case .min: + constraint = view.centerXAnchor.constraint( + greaterThanOrEqualTo: otherAnchor, + constant: length + ).with(priority: .messageSizeBounds) + case .max: + constraint = view.centerXAnchor.constraint( + lessThanOrEqualTo: otherAnchor, + constant: length + ).with(priority: .messageSizeBounds) + } + cachedConstraints.append(constraint) + } + if let y = center.y { + let length = self.length(for: y) { ($0.minY, $0.maxY) } + let otherAnchor: NSLayoutYAxisAnchor + switch y.boundary { + case .superview: otherAnchor = topAnchor + case .margin: otherAnchor = layoutMarginsGuide.topAnchor + case .safeArea: + if #available(iOS 11.0, *) { + otherAnchor = safeAreaLayoutGuide.topAnchor + } else { + otherAnchor = layoutMarginsGuide.topAnchor + } + } + let constraint: NSLayoutConstraint + switch relation { + case .equal: + constraint = view.centerYAnchor.constraint(equalTo: otherAnchor, constant: length) + .with(priority: .messageCenter) + case .min: + constraint = view.centerYAnchor.constraint( + greaterThanOrEqualTo: otherAnchor, + constant: length + ).with(priority: .messageSizeBounds) + case .max: + constraint = view.centerYAnchor.constraint( + lessThanOrEqualTo: otherAnchor, + constant: length + ).with(priority: .messageSizeBounds) + } + cachedConstraints.append(constraint) + } + } + + private func length( + for dimension: Layout.Size.Dimension, + extractor: (CGRect) -> CGFloat + ) -> CGFloat { + let insetBounds: CGRect = { + guard let boundary = dimension.boundary else { return .zero } + let insets: UIEdgeInsets + switch boundary { + case .superview: insets = .zero + case .margin: insets = layoutMargins + case .safeArea: + if #available(iOS 11.0, *) { + insets = safeAreaInsets + } else { + insets = layoutMargins + } + } + return bounds.inset(by: insets) + }() + switch dimension { + case .absolute(let dimension): + return dimension + case .relative(let percentage, _): + return extractor(insetBounds) * percentage + case .absoluteInsets(let dimension, _): + return extractor(insetBounds) - dimension * 2 + } + } + + private func length( + for dimension: Layout.Center.Dimension, + extractor: (CGRect) -> (CGFloat, CGFloat) + ) -> CGFloat { + let insets: UIEdgeInsets + switch dimension.boundary { + case .superview: insets = .zero + case .margin: insets = layoutMargins + case .safeArea: + if #available(iOS 11.0, *) { + insets = safeAreaInsets + } else { + insets = layoutMargins + } + } + let insetBounds = bounds.inset(by: insets) + switch dimension { + case .absolute(let dimension, _): + let (min, _) = extractor(insetBounds) + return min + dimension + case .relative(let percentage, _): + let (min, max) = extractor(insetBounds) + return min + (max - min) * percentage + } + } } diff --git a/SwiftMessages/MessageView.swift b/SwiftMessages/MessageView.swift index c71141a6..6e8cd087 100644 --- a/SwiftMessages/MessageView.swift +++ b/SwiftMessages/MessageView.swift @@ -55,7 +55,7 @@ open class MessageView: BaseView, Identifiable, AccessibleMessage { /// An optional button. This buttons' `.TouchUpInside` event will automatically /// invoke the optional `buttonTapHandler`, but its fine to add other target - /// action handlers can be added. + /// action handlers. @IBOutlet open var button: UIButton? { didSet { if let old = oldValue { diff --git a/SwiftMessages/PhysicsAnimation.swift b/SwiftMessages/PhysicsAnimation.swift index 0fb976c5..46f3d1ef 100644 --- a/SwiftMessages/PhysicsAnimation.swift +++ b/SwiftMessages/PhysicsAnimation.swift @@ -70,18 +70,41 @@ public class PhysicsAnimation: NSObject, Animator { messageView = view containerView = container self.context = context - view.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(view) + if let layoutDefiningView = view as? LayoutDefining & UIView { + container.install(layoutDefiningView: layoutDefiningView) + } else { + view.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(view) + } switch placement { case .center: - view.centerYAnchor.constraint(equalTo: container.centerYAnchor).with(priority: UILayoutPriority(200)).isActive = true + view.centerYAnchor.constraint(equalTo: container.centerYAnchor) + .with(priority: UILayoutPriority(200)).isActive = true case .top: - view.topAnchor.constraint(equalTo: container.topAnchor).with(priority: UILayoutPriority(200)).isActive = true + view.topAnchor.constraint(equalTo: container.topAnchor) + .with(priority: UILayoutPriority(200)).isActive = true case .bottom: - view.bottomAnchor.constraint(equalTo: container.bottomAnchor).with(priority: UILayoutPriority(200)).isActive = true + view.bottomAnchor.constraint(equalTo: container.bottomAnchor) + .with(priority: UILayoutPriority(200)).isActive = true } - NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: container, attribute: .leading, multiplier: 1, constant: 0).isActive = true - NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: container, attribute: .trailing, multiplier: 1, constant: 0).isActive = true + NSLayoutConstraint( + item: view, + attribute: .leading, + relatedBy: .equal, + toItem: container, + attribute: .leading, + multiplier: 1, + constant: 0 + ).with(priority: .belowMessageSizeable).isActive = true + NSLayoutConstraint( + item: view, + attribute: .trailing, + relatedBy: .equal, + toItem: container, + attribute: .trailing, + multiplier: 1, + constant: 0 + ).with(priority: .belowMessageSizeable).isActive = true // Important to layout now in order to get the right safe area insets container.layoutIfNeeded() adjustMargins() diff --git a/SwiftMessages/Presenter.swift b/SwiftMessages/Presenter.swift index 11243f54..1ba7aa68 100644 --- a/SwiftMessages/Presenter.swift +++ b/SwiftMessages/Presenter.swift @@ -105,9 +105,9 @@ class Presenter: NSObject { private static func animator(forPresentationStyle style: SwiftMessages.PresentationStyle, delegate: AnimationDelegate) -> Animator { switch style { case .top: - return TopBottomAnimation(style: .top, delegate: delegate) + return EdgeAnimation(style: .top, delegate: delegate) case .bottom: - return TopBottomAnimation(style: .bottom, delegate: delegate) + return EdgeAnimation(style: .bottom, delegate: delegate) case .center: return PhysicsAnimation(delegate: delegate) case .custom(let animator): diff --git a/SwiftMessages/Resources/CardView.xib b/SwiftMessages/Resources/CardView.xib index 425a6961..f3ad1df4 100644 --- a/SwiftMessages/Resources/CardView.xib +++ b/SwiftMessages/Resources/CardView.xib @@ -1,9 +1,9 @@ - + - + @@ -70,7 +70,6 @@ - @@ -78,25 +77,13 @@ - - - - - - - - - - - - @@ -119,18 +106,10 @@ - - - - - - - - diff --git a/SwiftMessages/Resources/CenteredView.xib b/SwiftMessages/Resources/CenteredView.xib index a9fda2be..b61242cc 100644 --- a/SwiftMessages/Resources/CenteredView.xib +++ b/SwiftMessages/Resources/CenteredView.xib @@ -1,9 +1,9 @@ - + - + @@ -32,7 +32,7 @@ - - - - - - - - - - @@ -96,10 +85,8 @@ - - @@ -122,18 +109,10 @@ - - - - - - - - diff --git a/SwiftMessages/Resources/TabView.xib b/SwiftMessages/Resources/TabView.xib index 141467c1..e572aa41 100644 --- a/SwiftMessages/Resources/TabView.xib +++ b/SwiftMessages/Resources/TabView.xib @@ -1,9 +1,9 @@ - + - + @@ -69,7 +69,6 @@ - @@ -78,27 +77,15 @@ - - - - - - - - - - - - @@ -121,16 +108,8 @@ - - - - - - - - diff --git a/SwiftMessages/SwiftMessagesSegue.swift b/SwiftMessages/SwiftMessagesSegue.swift index d4cba417..8013ccd0 100644 --- a/SwiftMessages/SwiftMessagesSegue.swift +++ b/SwiftMessages/SwiftMessagesSegue.swift @@ -69,60 +69,50 @@ import UIKit open class SwiftMessagesSegue: UIStoryboardSegue { /** - Specifies one of the pre-defined layouts, mirroring a subset of `MessageView.Layout`. + Specifies one of the pre-defined layout configurations. */ public enum Layout { - /// The standard message view layout on top. + /// The standard message view layout that slides down from the top edge. case topMessage - /// The standard message view layout on bottom. + /// The standard message view layout that slides up from the bottom edge. case bottomMessage - /// A floating card-style view with rounded corners on top - case topCard + /// The standard message view layout that slides in from the leading edge. + case leadingMessage - /// A floating tab-style view with rounded corners on bottom - case topTab + /// The standard message view layout that slides in from the trailing edge. + case trailingMessage - /// A floating card-style view with rounded corners on bottom + /// A floating card-style view with rounded corners that slides down from the top edge. + case topCard + + /// A floating card-style view with rounded corners that slides up from the bottom edge. case bottomCard - /// A floating tab-style view with rounded corners on top + /// A floating card-style view with rounded corners that slides in from the leading edge. + case leadingCard + + /// A floating card-style view with rounded corners that slides in from the trailing edge. + case trailingCard + + /// A floating tab-style view with rounded leading corners that slides down from the top edge. + case topTab + + /// A floating tab-style view with rounded leading corners that slides up from the bottom edge. case bottomTab + /// A floating tab-style view with rounded leading corners that slides in from the leading edge. + case leadingTab + + /// A floating tab-style view with rounded leading corners that slides in from the traling edge. + case trailingTab + /// A floating card-style view typically used with `.center` presentation style. case centered } - /** - Specifies how the view controller's view is installed into the - containing message view. - */ - public enum Containment { - - /** - The view controller's view is installed for edge-to-edge display, extending into the safe areas - to the device edges. This is done by calling `messageView.installContentView(:insets:)` - See that method's documentation for additional details. - */ - case content - - /** - The view controller's view is installed for card-style layouts, inset from the margins - and avoiding safe areas. This is done by calling `messageView.installBackgroundView(:insets:)`. - See that method's documentation for details. - */ - case background - - /** - The view controller's view is installed for tab-style layouts, inset from the side margins, but extending - to the device edge on the top or bottom. This is done by calling `messageView.installBackgroundVerticalView(:insets:)`. - See that method's documentation for details. - */ - case backgroundVertical - } - /// The presentation style to use. See the SwiftMessages.PresentationStyle for details. public var presentationStyle: SwiftMessages.PresentationStyle { get { return messenger.defaultConfig.presentationStyle } @@ -174,17 +164,11 @@ open class SwiftMessagesSegue: UIStoryboardSegue { /** The view controller's view is embedded in `containerView` before being installed into - `messageView`. This view provides configurable squircle (round) corners (see the parent + `messageView`. This view provides configurable continuous rounded corners (see the parent class `CornerRoundingView`). */ public var containerView: CornerRoundingView = CornerRoundingView() - /** - Specifies how the view controller's view is installed into the - containing message view. See `Containment` for details. - */ - public var containment: Containment = .content - /** Supply an instance of `KeyboardTrackingView` to have the message view avoid the keyboard. */ @@ -228,57 +212,130 @@ extension SwiftMessagesSegue { /// A convenience method for configuring some pre-defined layouts that mirror a subset of `MessageView.Layout`. public func configure(layout: Layout) { messageView.bounceAnimationOffset = 0 - containment = .content containerView.cornerRadius = 0 containerView.roundsLeadingCorners = false messageView.configureDropShadow() switch layout { case .topMessage: - messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) - messageView.collapseLayoutMarginAdditions = false - let animation = TopBottomAnimation(style: .top) +// TODO SIZE are these layout margin settings still relevant? +// messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) +// messageView.collapseLayoutMarginAdditions = false + let animation = EdgeAnimation(style: .top) animation.springDamping = 1 presentationStyle = .custom(animator: animation) case .bottomMessage: - messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) - messageView.collapseLayoutMarginAdditions = false - let animation = TopBottomAnimation(style: .bottom) +// messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) +// messageView.collapseLayoutMarginAdditions = false + let animation = EdgeAnimation(style: .bottom) + animation.springDamping = 1 + presentationStyle = .custom(animator: animation) + case .leadingMessage: +// messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) +// messageView.collapseLayoutMarginAdditions = false + let animation = EdgeAnimation(style: .leading) + animation.springDamping = 1 + presentationStyle = .custom(animator: animation) + case .trailingMessage: +// messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) +// messageView.collapseLayoutMarginAdditions = false + let animation = EdgeAnimation(style: .trailing) animation.springDamping = 1 presentationStyle = .custom(animator: animation) case .topCard: - containment = .background - messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - messageView.collapseLayoutMarginAdditions = true + messageView.layout.insets.top = .absolute(0, from: .safeArea) + messageView.layout.insets.leading = .absolute(0, from: .safeArea) + messageView.layout.insets.trailing = .absolute(0, from: .safeArea) + messageView.layout.min.insets.top = .absolute(10, from: .superview) + messageView.layout.min.insets.leading = .absolute(10, from: .superview) + messageView.layout.min.insets.trailing = .absolute(10, from: .superview) + messageView.layout.min.insets.bottom = .absolute(10, from: .safeArea) containerView.cornerRadius = 15 presentationStyle = .top case .bottomCard: - containment = .background - messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - messageView.collapseLayoutMarginAdditions = true + messageView.layout.insets.bottom = .absolute(0, from: .safeArea) + messageView.layout.insets.leading = .absolute(0, from: .safeArea) + messageView.layout.insets.trailing = .absolute(0, from: .safeArea) + messageView.layout.min.insets.bottom = .absolute(10, from: .superview) + messageView.layout.min.insets.leading = .absolute(10, from: .superview) + messageView.layout.min.insets.trailing = .absolute(10, from: .superview) + messageView.layout.min.insets.top = .absolute(10, from: .safeArea) containerView.cornerRadius = 15 presentationStyle = .bottom + case .leadingCard: + messageView.layout.insets.leading = .absolute(0, from: .safeArea) + messageView.layout.insets.top = .absolute(0, from: .safeArea) + messageView.layout.insets.bottom = .absolute(0, from: .safeArea) + messageView.layout.min.insets.leading = .absolute(10, from: .superview) + messageView.layout.min.insets.top = .absolute(10, from: .superview) + messageView.layout.min.insets.bottom = .absolute(10, from: .superview) + messageView.layout.min.insets.trailing = .absolute(10, from: .safeArea) + containerView.cornerRadius = 15 + let animation = EdgeAnimation(style: .leading) + presentationStyle = .custom(animator: animation) + case .trailingCard: + messageView.layout.insets.trailing = .absolute(0, from: .safeArea) + messageView.layout.insets.top = .absolute(0, from: .safeArea) + messageView.layout.insets.bottom = .absolute(0, from: .safeArea) + messageView.layout.min.insets.trailing = .absolute(10, from: .superview) + messageView.layout.min.insets.top = .absolute(10, from: .superview) + messageView.layout.min.insets.bottom = .absolute(10, from: .superview) + messageView.layout.min.insets.leading = .absolute(10, from: .safeArea) + containerView.cornerRadius = 15 + let animation = EdgeAnimation(style: .trailing) + presentationStyle = .custom(animator: animation) case .topTab: - containment = .backgroundVertical - messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10) - messageView.collapseLayoutMarginAdditions = true + messageView.layout.insets.top = .absolute(0, from: .superview) + messageView.layout.insets.leading = .absolute(0, from: .safeArea) + messageView.layout.insets.trailing = .absolute(0, from: .safeArea) + messageView.layout.min.insets.leading = .absolute(10, from: .superview) + messageView.layout.min.insets.trailing = .absolute(10, from: .superview) + messageView.layout.min.insets.bottom = .absolute(10, from: .safeArea) containerView.cornerRadius = 15 containerView.roundsLeadingCorners = true - let animation = TopBottomAnimation(style: .top) + let animation = EdgeAnimation(style: .top) animation.springDamping = 1 presentationStyle = .custom(animator: animation) case .bottomTab: - containment = .backgroundVertical - messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10) - messageView.collapseLayoutMarginAdditions = true + messageView.layout.insets.bottom = .absolute(0, from: .superview) + messageView.layout.insets.leading = .absolute(0, from: .safeArea) + messageView.layout.insets.trailing = .absolute(0, from: .safeArea) + messageView.layout.min.insets.leading = .absolute(10, from: .superview) + messageView.layout.min.insets.trailing = .absolute(10, from: .superview) + messageView.layout.min.insets.top = .absolute(10, from: .safeArea) + containerView.cornerRadius = 15 + containerView.roundsLeadingCorners = true + let animation = EdgeAnimation(style: .bottom) + animation.springDamping = 1 + presentationStyle = .custom(animator: animation) + case .leadingTab: + messageView.layout.insets.leading = .absolute(0, from: .superview) + messageView.layout.insets.top = .absolute(0, from: .safeArea) + messageView.layout.insets.bottom = .absolute(0, from: .safeArea) + messageView.layout.min.insets.top = .absolute(10, from: .superview) + messageView.layout.min.insets.bottom = .absolute(10, from: .superview) + messageView.layout.min.insets.trailing = .absolute(10, from: .safeArea) + containerView.cornerRadius = 15 + containerView.roundsLeadingCorners = true + let animation = EdgeAnimation(style: .leading) + animation.springDamping = 1 + presentationStyle = .custom(animator: animation) + case .trailingTab: + messageView.layout.insets.trailing = .absolute(0, from: .superview) + messageView.layout.insets.top = .absolute(0, from: .safeArea) + messageView.layout.insets.bottom = .absolute(0, from: .safeArea) + messageView.layout.min.insets.top = .absolute(10, from: .superview) + messageView.layout.min.insets.bottom = .absolute(10, from: .superview) + messageView.layout.min.insets.leading = .absolute(10, from: .safeArea) containerView.cornerRadius = 15 containerView.roundsLeadingCorners = true - let animation = TopBottomAnimation(style: .bottom) + let animation = EdgeAnimation(style: .trailing) animation.springDamping = 1 presentationStyle = .custom(animator: animation) case .centered: - containment = .background - messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - messageView.collapseLayoutMarginAdditions = true + messageView.layout.min.insets.top = .absolute(10, from: .safeArea) + messageView.layout.min.insets.bottom = .absolute(10, from: .safeArea) + messageView.layout.min.insets.leading = .absolute(10, from: .safeArea) + messageView.layout.min.insets.trailing = .absolute(10, from: .safeArea) containerView.cornerRadius = 15 presentationStyle = .center } @@ -343,29 +400,32 @@ extension SwiftMessagesSegue { } completeTransition = transitionContext.completeTransition let transitionContainer = transitionContext.containerView - toView.translatesAutoresizingMaskIntoConstraints = false - segue.containerView.addSubview(toView) - segue.containerView.topAnchor.constraint(equalTo: toView.topAnchor).isActive = true - segue.containerView.bottomAnchor.constraint(equalTo: toView.bottomAnchor).isActive = true - segue.containerView.leadingAnchor.constraint(equalTo: toView.leadingAnchor).isActive = true - segue.containerView.trailingAnchor.constraint(equalTo: toView.trailingAnchor).isActive = true - // Install the `toView` into the message view. - switch segue.containment { - case .content: - segue.messageView.installContentView(segue.containerView) - case .background: + // Install the background and content views + do { segue.messageView.installBackgroundView(segue.containerView) - case .backgroundVertical: - segue.messageView.installBackgroundVerticalView(segue.containerView) + segue.messageView.installContentView(toView) } + let toVC = transitionContext.viewController(forKey: .to) + + // Nav controller automatically includes height of nav bar in, + // the `preferredContentSize` and our logic needs to consider this. + var navInset: CGFloat = 0 + if let nav = toVC as? UINavigationController { + navInset = nav.navigationBar.frame.height + } + if let preferredHeight = toVC?.preferredContentSize.height, - preferredHeight > 0 { - segue.containerView.heightAnchor.constraint(equalToConstant: preferredHeight).with(priority: UILayoutPriority(rawValue: 951)).isActive = true + preferredHeight - navInset > 0 { + segue.containerView.heightAnchor.constraint(equalToConstant: preferredHeight) + .with(priority: .belowMessageSizeable - 1) + .isActive = true } if let preferredWidth = toVC?.preferredContentSize.width, preferredWidth > 0 { - segue.containerView.widthAnchor.constraint(equalToConstant: preferredWidth).with(priority: UILayoutPriority(rawValue: 951)).isActive = true + segue.containerView.widthAnchor.constraint(equalToConstant: preferredWidth) + .with(priority: .belowMessageSizeable - 1) + .isActive = true } segue.presenter.config.presentationContext = .view(transitionContainer) segue.messenger.show(presenter: segue.presenter) diff --git a/SwiftMessages/UILayoutPriority+Extensions.swift b/SwiftMessages/UILayoutPriority+Extensions.swift new file mode 100644 index 00000000..ae6dc07e --- /dev/null +++ b/SwiftMessages/UILayoutPriority+Extensions.swift @@ -0,0 +1,26 @@ +// +// UILayoutPriority+Extensions.swift +// SwiftMessages +// +// Created by Timothy Moose on 2/14/21. +// Copyright © 2021 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +/// The priority used for `MessageSizeable` constraints +extension UILayoutPriority { + /// A constraint priority higher than those used for `MessageSizeable` + public static let aboveMessageSizeable: UILayoutPriority = messageInsetsBounds + 1 + + /// A constraint priority lower than those used for `MessageSizeable` + public static let belowMessageSizeable: UILayoutPriority = messageCenter - 1 + + static let messageCenter: UILayoutPriority = UILayoutPriority(900) + static let messageSize: UILayoutPriority = UILayoutPriority(901) + static let messageInsets: UILayoutPriority = UILayoutPriority(902) + static let messageCenterBounds: UILayoutPriority = UILayoutPriority(903) + static let messageSizeBounds: UILayoutPriority = UILayoutPriority(904) + static let messageInsetsBounds: UILayoutPriority = UILayoutPriority(905) +} + diff --git a/SwiftMessages/UIViewController+Extensions.swift b/SwiftMessages/UIViewController+Extensions.swift index 7fbd4dd1..6cedc0d5 100644 --- a/SwiftMessages/UIViewController+Extensions.swift +++ b/SwiftMessages/UIViewController+Extensions.swift @@ -87,14 +87,14 @@ extension UIViewController { } extension SwiftMessages.PresentationStyle { - /// A temporary workaround to allow custom presentation contexts using `TopBottomAnimation` + /// A temporary workaround to allow custom presentation contexts using `EdgeAnimation` /// to display properly behind bars. THe long term solution is to refactor all of the /// presentation context logic to work with safe area insets. - var topBottomStyle: TopBottomAnimation.Style? { + var topBottomStyle: EdgeAnimation.Style? { switch self { case .top: return .top case .bottom: return .bottom - case .custom(let animator): return (animator as? TopBottomAnimation)?.style + case .custom(let animator): return (animator as? EdgeAnimation)?.style case .center: return nil } } From 0699288fc3d5772240279a06b8a253da0ac65ae9 Mon Sep 17 00:00:00 2001 From: Timothy Moose Date: Fri, 23 Jun 2023 16:36:20 -0500 Subject: [PATCH 4/8] Auto-release if needed --- SwiftMessages/SwiftMessagesSegue.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/SwiftMessages/SwiftMessagesSegue.swift b/SwiftMessages/SwiftMessagesSegue.swift index 8013ccd0..45c7581c 100644 --- a/SwiftMessages/SwiftMessagesSegue.swift +++ b/SwiftMessages/SwiftMessagesSegue.swift @@ -192,6 +192,7 @@ open class SwiftMessagesSegue: UIStoryboardSegue { override open func perform() { (source as? WindowViewController)?.install() selfRetainer = self + startReleaseMonitor() if overrideModalPresentationStyle { destination.modalPresentationStyle = .custom } @@ -206,6 +207,19 @@ open class SwiftMessagesSegue: UIStoryboardSegue { } fileprivate let safeAreaWorkaroundViewController = UIViewController() + + /// The self-retainer will not allow the segue, presenting and presented view controllers to be released if the presenting view controller + /// is removed without first dismissing. This monitor handles that scenario by setting `self.selfRetainer = nil` if + /// the presenting view controller is no longer in the heirarchy. + private func startReleaseMonitor() { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in + guard let self = self else { return } + switch self.source.parent { + case .none: self.selfRetainer = nil + case .some: self.startReleaseMonitor() + } + } + } } extension SwiftMessagesSegue { From 5b8ff0d6ff39fe83376ccbfbd01495aad156a6a7 Mon Sep 17 00:00:00 2001 From: Timothy Moose Date: Fri, 23 Jun 2023 16:39:30 -0500 Subject: [PATCH 5/8] Try again --- SwiftMessages/SwiftMessagesSegue.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftMessages/SwiftMessagesSegue.swift b/SwiftMessages/SwiftMessagesSegue.swift index 45c7581c..f0027f41 100644 --- a/SwiftMessages/SwiftMessagesSegue.swift +++ b/SwiftMessages/SwiftMessagesSegue.swift @@ -214,7 +214,7 @@ open class SwiftMessagesSegue: UIStoryboardSegue { private func startReleaseMonitor() { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in guard let self = self else { return } - switch self.source.parent { + switch self.source.view.window { case .none: self.selfRetainer = nil case .some: self.startReleaseMonitor() } From e58c289bfb3226a1e995c327aa820c94f2998b07 Mon Sep 17 00:00:00 2001 From: Timothy Moose Date: Fri, 23 Jun 2023 16:46:07 -0500 Subject: [PATCH 6/8] Debugging --- SwiftMessages/SwiftMessagesSegue.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SwiftMessages/SwiftMessagesSegue.swift b/SwiftMessages/SwiftMessagesSegue.swift index f0027f41..0cdeeff6 100644 --- a/SwiftMessages/SwiftMessagesSegue.swift +++ b/SwiftMessages/SwiftMessagesSegue.swift @@ -359,12 +359,13 @@ extension SwiftMessagesSegue { extension SwiftMessagesSegue: UIViewControllerTransitioningDelegate { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { let shower = TransitioningPresenter(segue: self) - messenger.defaultConfig.eventListeners.append { [unowned self] in + let hider = self.hider + messenger.defaultConfig.eventListeners.append { [weak self] in switch $0 { case .didShow: shower.completeTransition?(true) case .didHide: - if let completeTransition = self.hider.completeTransition { + if let completeTransition = hider.completeTransition { completeTransition(true) } else { // Case where message is internally hidden by SwiftMessages, such as with a @@ -372,7 +373,7 @@ extension SwiftMessagesSegue: UIViewControllerTransitioningDelegate { source.dismiss(animated: false, completion: nil) } (source as? WindowViewController)?.uninstall() - self.selfRetainer = nil + self?.selfRetainer = nil default: break } } From f70a8862923f99c07bd4d76d1cce0979f3a73e06 Mon Sep 17 00:00:00 2001 From: Mofe Ejegi Date: Mon, 26 Jan 2026 02:59:32 +0100 Subject: [PATCH 7/8] Enhance EdgeAnimation with leading and trailing styles; update storyboard and fix comments --- Demo/Demo/Base.lproj/Main.storyboard | 233 +++++++++++------------ SwiftMessages/EdgeAnimation.swift | 8 +- SwiftMessages/Layout.swift | 2 +- SwiftMessages/Presenter.swift | 6 +- SwiftMessages/SwiftMessages.swift | 10 + SwiftMessages/SwiftMessagesSegue.swift | 4 +- SwiftMessages/TopBottomPresentable.swift | 8 +- 7 files changed, 145 insertions(+), 126 deletions(-) diff --git a/Demo/Demo/Base.lproj/Main.storyboard b/Demo/Demo/Base.lproj/Main.storyboard index 6417a0f8..c38d6707 100644 --- a/Demo/Demo/Base.lproj/Main.storyboard +++ b/Demo/Demo/Base.lproj/Main.storyboard @@ -2,9 +2,9 @@ - - - + + + @@ -847,113 +847,113 @@ - - - - - - - + + + + + + + - - - - + + + + @@ -966,7 +966,7 @@ - + @@ -1096,7 +1096,6 @@ - @@ -1122,15 +1121,15 @@ - + - - - - - - - - - + + + + + + + + + diff --git a/SwiftMessages/EdgeAnimation.swift b/SwiftMessages/EdgeAnimation.swift index 26f286c1..fcee1ee1 100644 --- a/SwiftMessages/EdgeAnimation.swift +++ b/SwiftMessages/EdgeAnimation.swift @@ -8,10 +8,10 @@ import UIKit -@MainActor @available(*, deprecated, message: "Class renamed to `EdgeAnimation` to reflect new ability to do leading and trailing animations.") public typealias TopBottomAnimation = EdgeAnimation +@MainActor public class EdgeAnimation: NSObject, Animator { public enum Style { @@ -22,7 +22,7 @@ public class EdgeAnimation: NSObject, Animator { } public weak var delegate: AnimationDelegate? - public let style: TopBottomAnimationStyle + public let style: Style public var showDuration: TimeInterval = 0.35 @@ -46,11 +46,11 @@ public class EdgeAnimation: NSObject, Animator { weak var containerView: UIView? var context: AnimationContext? - public init(style: TopBottomAnimationStyle) { + public init(style: Style) { self.style = style } - init(style: TopBottomAnimationStyle, delegate: AnimationDelegate) { + init(style: Style, delegate: AnimationDelegate) { self.style = style self.delegate = delegate } diff --git a/SwiftMessages/Layout.swift b/SwiftMessages/Layout.swift index 09c2c16f..09223014 100644 --- a/SwiftMessages/Layout.swift +++ b/SwiftMessages/Layout.swift @@ -1,5 +1,5 @@ // -// BoundaryInsets.swift +// Layout.swift // SwiftMessages // // Created by Timothy Moose on 2/13/21. diff --git a/SwiftMessages/Presenter.swift b/SwiftMessages/Presenter.swift index 1050d7c7..12f20b9e 100644 --- a/SwiftMessages/Presenter.swift +++ b/SwiftMessages/Presenter.swift @@ -70,7 +70,7 @@ class Presenter: NSObject { return duration } - /// Detects the scenario where the view was shown, but the containing view heirarchy was removed before the view + /// Detects the scenario where the view was shown, but the containing view hierarchy was removed before the view /// was hidden. This unusual scenario could result in the message queue being blocked because the presented /// view was not properly hidden by SwiftMessages. `isOrphaned` allows the queuing logic to unblock the queue. var isOrphaned: Bool { @@ -118,6 +118,10 @@ class Presenter: NSObject { return EdgeAnimation(style: .top, delegate: delegate) case .bottom: return EdgeAnimation(style: .bottom, delegate: delegate) + case .leading: + return EdgeAnimation(style: .leading, delegate: delegate) + case .trailing: + return EdgeAnimation(style: .trailing, delegate: delegate) case .center: return PhysicsAnimation(delegate: delegate) case .custom(let animator): diff --git a/SwiftMessages/SwiftMessages.swift b/SwiftMessages/SwiftMessages.swift index f05ad494..23f056db 100644 --- a/SwiftMessages/SwiftMessages.swift +++ b/SwiftMessages/SwiftMessages.swift @@ -34,6 +34,16 @@ open class SwiftMessages { */ case bottom + /** + Message view slides in from the leading edge. + */ + case leading + + /** + Message view slides in from the trailing edge. + */ + case trailing + /** Message view fades into the center. */ diff --git a/SwiftMessages/SwiftMessagesSegue.swift b/SwiftMessages/SwiftMessagesSegue.swift index d4b70442..6465ab12 100644 --- a/SwiftMessages/SwiftMessagesSegue.swift +++ b/SwiftMessages/SwiftMessagesSegue.swift @@ -106,7 +106,7 @@ open class SwiftMessagesSegue: UIStoryboardSegue { /// A floating tab-style view with rounded leading corners that slides in from the leading edge. case leadingTab - /// A floating tab-style view with rounded leading corners that slides in from the traling edge. + /// A floating tab-style view with rounded leading corners that slides in from the trailing edge. case trailingTab /// A floating card-style view typically used with `.center` presentation style. @@ -209,7 +209,7 @@ open class SwiftMessagesSegue: UIStoryboardSegue { fileprivate let safeAreaWorkaroundViewController = UIViewController() /// The self-retainer will not allow the segue, presenting and presented view controllers to be released if the presenting view controller /// is removed without first dismissing. This monitor handles that scenario by setting `self.selfRetainer = nil` if -/// the presenting view controller is no longer in the heirarchy. +/// the presenting view controller is no longer in the hierarchy. private func startReleaseMonitor() { Task { @MainActor [weak self] in try? await Task.sleep(seconds: 2) diff --git a/SwiftMessages/TopBottomPresentable.swift b/SwiftMessages/TopBottomPresentable.swift index 18804160..7d10b9f7 100644 --- a/SwiftMessages/TopBottomPresentable.swift +++ b/SwiftMessages/TopBottomPresentable.swift @@ -17,7 +17,13 @@ protocol TopBottomPresentable { // MARK: - TopBottom Presentable Conformances extension TopBottomAnimation: TopBottomPresentable { - var topBottomStyle: TopBottomAnimationStyle? { return style } + var topBottomStyle: TopBottomAnimationStyle? { + switch style { + case .top: return .top + case .bottom: return .bottom + case .leading, .trailing: return nil + } + } } extension PhysicsAnimation: TopBottomPresentable { From a133ed60aa544ea86a195b1720b9de8820cb6abe Mon Sep 17 00:00:00 2001 From: Mofe Ejegi Date: Mon, 26 Jan 2026 03:58:56 +0100 Subject: [PATCH 8/8] Revert hide animation for EdgeAnimations --- SwiftMessages/EdgeAnimation.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SwiftMessages/EdgeAnimation.swift b/SwiftMessages/EdgeAnimation.swift index fcee1ee1..601f82ae 100644 --- a/SwiftMessages/EdgeAnimation.swift +++ b/SwiftMessages/EdgeAnimation.swift @@ -65,7 +65,7 @@ public class EdgeAnimation: NSObject, Animator { NotificationCenter.default.removeObserver(self) let view = context.messageView self.context = context - UIView.animate(withDuration: hideDuration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.beginFromCurrentState]) { + UIView.animate(withDuration: hideDuration, delay: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: { switch self.style { case .top: view.transform = CGAffineTransform(translationX: 0, y: -view.frame.height) @@ -76,14 +76,14 @@ public class EdgeAnimation: NSObject, Animator { case .trailing: view.transform = CGAffineTransform(translationX: view.frame.maxX + view.frame.width, y: 0) } - } completion: { completed in + }, completion: { completed in #if SWIFTMESSAGES_APP_EXTENSIONS completion(completed) #else // Fix #131 by always completing if application isn't active. completion(completed || UIApplication.shared.applicationState != .active) #endif - } + }) } func install(context: AnimationContext) {