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.xcodeproj/project.pbxproj b/SwiftMessages.xcodeproj/project.pbxproj index e25534bd..741d1bb4 100644 --- a/SwiftMessages.xcodeproj/project.pbxproj +++ b/SwiftMessages.xcodeproj/project.pbxproj @@ -55,13 +55,15 @@ 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 */; }; 228F7DDE2ACF703A006C9644 /* MessageHostingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DDB2ACF7039006C9644 /* MessageHostingView.swift */; }; 228F7DDF2ACF703A006C9644 /* SwiftMessageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DDC2ACF703A006C9644 /* SwiftMessageModifier.swift */; }; 228F7DE02ACF703A006C9644 /* MessageViewConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DDD2ACF703A006C9644 /* MessageViewConvertible.swift */; }; 22982C172B6030B000852311 /* HapticMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22982C162B6030B000852311 /* HapticMessage.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 */; }; 22D3B4562B1CEF76002D8665 /* Task+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D3B4552B1CEF76002D8665 /* Task+Extensions.swift */; }; 22DFC9161EFF30F6001B1CA1 /* CenteredView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 22DFC9151EFF30F6001B1CA1 /* CenteredView.xib */; }; @@ -152,13 +154,15 @@ 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 = ""; }; 228F7DDB2ACF7039006C9644 /* MessageHostingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHostingView.swift; sourceTree = ""; }; 228F7DDC2ACF703A006C9644 /* SwiftMessageModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftMessageModifier.swift; sourceTree = ""; }; 228F7DDD2ACF703A006C9644 /* MessageViewConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageViewConvertible.swift; sourceTree = ""; }; 22982C162B6030B000852311 /* HapticMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticMessage.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 = ""; }; + 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 = ""; }; 22D3B4552B1CEF76002D8665 /* Task+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Extensions.swift"; sourceTree = ""; }; @@ -231,6 +235,7 @@ children = ( 220655111FAF82B600F4E00F /* MarginAdjustable+Extensions.swift */, 22774B9F20B5EF2A00813732 /* UIEdgeInsets+Extensions.swift */, + 2290958025D9D407002E8111 /* UILayoutPriority+Extensions.swift */, 229F778025FAB1E9008C2ACB /* UIWindow+Extensions.swift */, 22D3B4552B1CEF76002D8665 /* Task+Extensions.swift */, ); @@ -240,7 +245,7 @@ 2244656C1EF1D62700C50413 /* Animations */ = { isa = PBXGroup; children = ( - 2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */, + 2298C2081EE486E300E2DDC1 /* EdgeAnimation.swift */, 2270044A1FAFA6DD0045DDC3 /* PhysicsAnimation.swift */, 22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */, ); @@ -334,6 +339,7 @@ 22982C162B6030B000852311 /* HapticMessage.swift */, 864495551D4F7C390056EB2A /* Identifiable.swift */, 225304652293000C00A03ACF /* KeyboardTrackingView.swift */, + 2290944725D88A05002E8111 /* Layout.swift */, 86AAF81D1D5549680031EE32 /* MarginAdjustable.swift */, 86AAF82C1D580F410031EE32 /* Theme.swift */, 224C3C922C28BC4400B50B18 /* TopBottomAnimationStyle.swift */, @@ -572,6 +578,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 */, 22D3B4562B1CEF76002D8665 /* Task+Extensions.swift in Sources */, @@ -600,13 +607,14 @@ 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 */, 0797E40E26EE12B400691606 /* WindowScene.swift in Sources */, 225304622290C76E00A03ACF /* NSLayoutConstraint+Extensions.swift in Sources */, 223DE69D2C29E50C000161E5 /* MessageGeometryProxy.swift in Sources */, 86BBA9071D5E040C00FE8F16 /* MarginAdjustable.swift in Sources */, 867BED211D622793005212E3 /* BackgroundViewable.swift in Sources */, + 2290958125D9D407002E8111 /* UILayoutPriority+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SwiftMessages/Animator.swift b/SwiftMessages/Animator.swift index c79d0f0e..60ab962d 100644 --- a/SwiftMessages/Animator.swift +++ b/SwiftMessages/Animator.swift @@ -45,13 +45,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 c41d166b..8831ea0e 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,7 +267,6 @@ open class BaseView: UIView, BackgroundViewable, MarginAdjustable { } private var backgroundHeightConstraint: NSLayoutConstraint? - /* Mark: - Layout */ 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 49a45adc..601f82ae 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,16 +8,25 @@ import UIKit +@available(*, deprecated, message: "Class renamed to `EdgeAnimation` to reflect new ability to do leading and trailing animations.") +public typealias TopBottomAnimation = EdgeAnimation + @MainActor -public class TopBottomAnimation: NSObject, Animator { +public class EdgeAnimation: NSObject, Animator { + public enum Style { + case top + case bottom + case leading + case trailing + } public weak var delegate: AnimationDelegate? - public let style: TopBottomAnimationStyle + 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 @@ -37,11 +46,11 @@ public class TopBottomAnimation: 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 } @@ -62,6 +71,10 @@ public class TopBottomAnimation: NSObject, Animator { 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 #if SWIFTMESSAGES_APP_EXTENSIONS @@ -82,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 { @@ -118,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] } } } @@ -133,6 +190,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 } @@ -168,21 +229,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 { @@ -191,6 +270,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 } @@ -203,19 +286,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..09223014 --- /dev/null +++ b/SwiftMessages/Layout.swift @@ -0,0 +1,104 @@ +// +// Layout.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 d6313427..bf8d937c 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) @@ -76,4 +114,366 @@ class MaskingView: PassthroughView { constant: offset ).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 6384917e..7ac5d4b0 100644 --- a/SwiftMessages/MessageView.swift +++ b/SwiftMessages/MessageView.swift @@ -62,7 +62,7 @@ open class MessageView: BaseView, Identifiable, AccessibleMessage, HapticMessage /// 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 ef8476f7..65642b58 100644 --- a/SwiftMessages/PhysicsAnimation.swift +++ b/SwiftMessages/PhysicsAnimation.swift @@ -64,10 +64,10 @@ public class PhysicsAnimation: NSObject, Animator { UIView.animate( withDuration: hideDuration, delay: 0, - options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction], + options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction], animations: { view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) - }, + }, completion: nil ) UIView.animate( @@ -88,27 +88,22 @@ 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.activate([ view.leadingAnchor.constraint(equalTo: container.leadingAnchor), diff --git a/SwiftMessages/Presenter.swift b/SwiftMessages/Presenter.swift index 76bd631c..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 { @@ -115,9 +115,13 @@ 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 .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/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/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 35687e14..6465ab12 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 trailing 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. */ @@ -223,77 +207,149 @@ 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() { - Task { @MainActor [weak self] in - try? await Task.sleep(seconds: 2) - guard let self = self else { return } - switch self.source.view.window { - case .none: self.selfRetainer = nil - case .some: self.startReleaseMonitor() - } +/// 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 hierarchy. +private func startReleaseMonitor() { + Task { @MainActor [weak self] in + try? await Task.sleep(seconds: 2) + guard let self = self else { return } + switch self.source.view.window { + case .none: self.selfRetainer = nil + case .some: self.startReleaseMonitor() } } } +} 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 } @@ -351,29 +407,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/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 { 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) +} +