From f0b573cc1177e70ac892404f3dca527b00e4ab98 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 16:11:33 +0100 Subject: [PATCH 01/15] Minor `PostView` clean up --- Sources/Views/PostView/PostView.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/Views/PostView/PostView.swift b/Sources/Views/PostView/PostView.swift index fa7314b..664c56a 100644 --- a/Sources/Views/PostView/PostView.swift +++ b/Sources/Views/PostView/PostView.swift @@ -125,9 +125,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { /// and therefore should be ignored." /// /// - Parameter view: `UIView` - private func hiddenTranslationY( - for view: UIView - ) -> CGAffineTransform { + private func hiddenTranslationY(for view: UIView) -> CGAffineTransform { return CGAffineTransform( translationX: 0, y: -(view.bounds.size.height + edgeInsets.top) @@ -230,9 +228,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { didRemove view: UIView ) { // Should check to remove self - guard removeFromSuperviewOnEmpty, !postManager.isActive else { - return - } + guard removeFromSuperviewOnEmpty, !postManager.isActive else { return } removeFromSuperview() } } From 41fe23058eda69571163236c6f5951be1711589d Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 16:12:37 +0100 Subject: [PATCH 02/15] Minor `PostManager` clean up --- Sources/PostManager/PostManager.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/PostManager/PostManager.swift b/Sources/PostManager/PostManager.swift index 081e5e6..0ef416e 100644 --- a/Sources/PostManager/PostManager.swift +++ b/Sources/PostManager/PostManager.swift @@ -26,7 +26,7 @@ public class PostManager { /// `UIViewPoster` to handle the post and remove of a `PostRequest` private weak var poster: UIViewPoster? - /// `PostManagerDelegate` for `PostManager` delegate callabacks + /// `PostManagerDelegate` for `PostManager` delegate callbacks public weak var delegate: PostManagerDelegate? /// `PostGestureManager` to manage `UIGestureRecognizer` actions @@ -48,7 +48,7 @@ public class PostManager { /// Should the posted `UIView`s be handled by a serial `Queue`, i.e. one at a time. /// If `true`, queued `PostRequest`s are stored in `queue`. - public var isSerialQueue: Bool = true { + public var isSerialQueue = true { didSet { guard !isSerialQueue else { return } while let postRequest = queue.dequeue() { @@ -107,10 +107,10 @@ public class PostManager { /// Is the `PostManager` posting or scheduled to post public var isActive: Bool { - // A post is showing atm + // Is a post is showing at the moment let isPosting = !currentPostRequests.isEmpty - // A post is scheduled to show in the future + // Is a post is scheduled to show in the future let isQueued = !queue.isEmpty return isPosting || isQueued From 11e7f3ef225c70b008c835bbdb5488d2001f955d Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 16:12:54 +0100 Subject: [PATCH 03/15] Create `Order` for use in `PostView` --- MessageStackView.xcodeproj/project.pbxproj | 4 +++ Sources/Models/Order.swift | 35 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 Sources/Models/Order.swift diff --git a/MessageStackView.xcodeproj/project.pbxproj b/MessageStackView.xcodeproj/project.pbxproj index 64936c3..a2baa79 100644 --- a/MessageStackView.xcodeproj/project.pbxproj +++ b/MessageStackView.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ B81AEE5024FFF27A0068CE23 /* ShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE4F24FFF27A0068CE23 /* ShadowLayer.swift */; }; B81AEE5224FFFB2E0068CE23 /* ParentShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE5124FFFB2E0068CE23 /* ParentShadowLayer.swift */; }; B81AEE5425003F270068CE23 /* ShadowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE5325003F270068CE23 /* ShadowViewController.swift */; }; + B83CACB12667D5CE008FB755 /* Order.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83CACB02667D5CE008FB755 /* Order.swift */; }; B872BE8725A3874C0031D619 /* UIImageView+Hidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = B872BE8625A3874C0031D619 /* UIImageView+Hidden.swift */; }; B872BE9125A387570031D619 /* UILabel+Hidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = B872BE9025A387570031D619 /* UILabel+Hidden.swift */; }; B875C98925126EB200FA05B5 /* UIViewController+System.swift in Sources */ = {isa = PBXBuildFile; fileRef = B875C98825126EB200FA05B5 /* UIViewController+System.swift */; }; @@ -172,6 +173,7 @@ B81AEE4F24FFF27A0068CE23 /* ShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowLayer.swift; sourceTree = ""; }; B81AEE5124FFFB2E0068CE23 /* ParentShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentShadowLayer.swift; sourceTree = ""; }; B81AEE5325003F270068CE23 /* ShadowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowViewController.swift; sourceTree = ""; }; + B83CACB02667D5CE008FB755 /* Order.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Order.swift; sourceTree = ""; }; B872BE8625A3874C0031D619 /* UIImageView+Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Hidden.swift"; sourceTree = ""; }; B872BE9025A387570031D619 /* UILabel+Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Hidden.swift"; sourceTree = ""; }; B875C98825126EB200FA05B5 /* UIViewController+System.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+System.swift"; sourceTree = ""; }; @@ -519,6 +521,7 @@ OBJ_54 /* PostAnimation.swift */, OBJ_55 /* Queue.swift */, B80B92AA2527324400F97364 /* Vector3.swift */, + B83CACB02667D5CE008FB755 /* Order.swift */, ); path = Models; sourceTree = ""; @@ -874,6 +877,7 @@ B8F1C76324D8198E007C2712 /* ApplicationPostView.swift in Sources */, B81727F024D6CC9F0062282B /* PostViewController.swift in Sources */, B8E1CEEF2500F88C002BFE77 /* NeuomorphicShadow.swift in Sources */, + B83CACB12667D5CE008FB755 /* Order.swift in Sources */, OBJ_115 /* BadgeMessageView.swift in Sources */, OBJ_116 /* BadgeMessageViewable.swift in Sources */, OBJ_117 /* Poster+BadgeMessage.swift in Sources */, diff --git a/Sources/Models/Order.swift b/Sources/Models/Order.swift new file mode 100644 index 0000000..958b291 --- /dev/null +++ b/Sources/Models/Order.swift @@ -0,0 +1,35 @@ +// +// Order.swift +// MessageStackView +// +// Created by Ben Shutt on 02/06/2021. +// Copyright © 2021 3 SIDED CUBE APP PRODUCTIONS LTD. All rights reserved. +// + +import Foundation + +/// How posted `UIView`s are ordered. +/// E.g. the order of the `arrangedSubviews`. +public enum Order { + + /// Natural order of `UIStackView`s. Posted `UIView`s get appended to the + /// `arrangedSubviews` array appearing below/after the previous. + case topToBottom + + /// Reverse order of `UIStackView`s. Posted `UIView`s get inserted at the start of the + /// `arrangedSubviews` array appearing above/before the previous. + case bottomToTop +} + +// MARK: - Extensions + +public extension Order { + + /// Other `Order` (opposite direction) + var switched: Self { + switch self { + case .topToBottom: return .bottomToTop + case .bottomToTop: return .topToBottom + } + } +} From aff2f0bc88ed3db12ce857070cd9c6f56e51384c Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 16:13:08 +0100 Subject: [PATCH 04/15] Use `Order` in `MessageStackView` --- .../MessageViewController.swift | 2 +- Sources/Constraints/MessageLayout.swift | 13 ++++ Sources/PostManager/UIView+Poster.swift | 4 +- .../ConnectivityManager+MessageManager.swift | 2 +- .../ConnectivityViewController.swift | 2 +- Sources/Views/PostView/MessageStackView.swift | 60 ++++--------------- 6 files changed, 30 insertions(+), 53 deletions(-) diff --git a/Example/ViewControllers/MessageViewController.swift b/Example/ViewControllers/MessageViewController.swift index 11d240a..87171b2 100644 --- a/Example/ViewControllers/MessageViewController.swift +++ b/Example/ViewControllers/MessageViewController.swift @@ -13,7 +13,7 @@ class MessageViewController: UIViewController { private lazy var messageStackView: MessageStackView = { let messageStackView: MessageStackView = view.createMessageStackView() - messageStackView.messageConfiguation = MessageConfiguration( + messageStackView.messageConfiguration = MessageConfiguration( backgroundColor: .systemGroupedBackground, tintColor: .gray, shadow: true, diff --git a/Sources/Constraints/MessageLayout.swift b/Sources/Constraints/MessageLayout.swift index cd06ef4..b3a9757 100644 --- a/Sources/Constraints/MessageLayout.swift +++ b/Sources/Constraints/MessageLayout.swift @@ -21,6 +21,19 @@ public enum MessageLayout: Int { case bottom } +// MARK: - Order + +public extension MessageLayout { + + /// Map to `Order` + func toOrder() -> Order { + switch self { + case .top: return .topToBottom + case .bottom: return .bottomToTop + } + } +} + // MARK: - NSLayoutConstraint public extension MessageLayout { diff --git a/Sources/PostManager/UIView+Poster.swift b/Sources/PostManager/UIView+Poster.swift index 6afefc6..196e4e7 100644 --- a/Sources/PostManager/UIView+Poster.swift +++ b/Sources/PostManager/UIView+Poster.swift @@ -1,5 +1,5 @@ // -// UIView+MessageStackView.swift +// UIView+Poster.swift // MessageStackView // // Created by Ben Shutt on 03/07/2020. @@ -48,7 +48,7 @@ public extension UIView { layout: layout, constrainToSafeArea: constrainToSafeArea ) - messageStackView.updateOrderForLayout(layout) + messageStackView.order = layout.toOrder() return messageStackView } diff --git a/Sources/Reachability/ConnectivityManager+MessageManager.swift b/Sources/Reachability/ConnectivityManager+MessageManager.swift index 3e102e0..720e758 100644 --- a/Sources/Reachability/ConnectivityManager+MessageManager.swift +++ b/Sources/Reachability/ConnectivityManager+MessageManager.swift @@ -21,7 +21,7 @@ public extension ConnectivityManager { /// `MessageStackView ` to post messages public private(set) lazy var messageStackView: MessageStackView = { let messageStackView = MessageStackView() - messageStackView.updateOrderForLayout(.bottom) + messageStackView.order = MessageLayout.bottom.toOrder() messageStackView.postManager.delegate = self return messageStackView }() diff --git a/Sources/Reachability/ConnectivityViewController.swift b/Sources/Reachability/ConnectivityViewController.swift index fee5b04..d939317 100644 --- a/Sources/Reachability/ConnectivityViewController.swift +++ b/Sources/Reachability/ConnectivityViewController.swift @@ -38,7 +38,7 @@ open class ConnectivityViewController: UIViewController, ConnectivityMessageable layout: messageLayout, constrainToSafeArea: false // Inset spaceView height ) - messageStackView.updateOrderForLayout(messageLayout) + messageStackView.order = messageLayout.toOrder() view.setNeedsLayout() } diff --git a/Sources/Views/PostView/MessageStackView.swift b/Sources/Views/PostView/MessageStackView.swift index 5345989..085968b 100644 --- a/Sources/Views/PostView/MessageStackView.swift +++ b/Sources/Views/PostView/MessageStackView.swift @@ -11,18 +11,6 @@ import UIKit /// A `UIStackView` with a `PostManager` for posting, queueing and removing `UIView`s open class MessageStackView: UIStackView, Poster { - /// How posted `UIView`s are ordered in the `arrangedSubviews`. - public enum Order { - - /// Natural order of `UIStackView`s. Posted `UIView`s get appended to the - /// `arrangedSubviews` array appearing below/after the previous. - case `default` // topToBottom - - /// Reverese order of `UIStackView`s. Posted `UIView`s get inserted at the start of the - /// `arrangedSubviews` array appearing above/before the previous. - case reversed // bottomToTop - } - /// `PostManager` to manage posting, queueing, removing of `PostRequest`s public private(set) lazy var postManager: PostManager = { let postManager = PostManager(poster: self) @@ -31,24 +19,24 @@ open class MessageStackView: UIStackView, Poster { }() /// Default `MessageConfiguration` to apply to posted `UIView`s - public var messageConfiguation = MessageConfiguration() { + public var messageConfiguration = MessageConfiguration() { didSet { - guard messageConfiguation.applyToAll else { + guard messageConfiguration.applyToAll else { return } - // If the `messageConfiguation` has updated, update the current `UIView`s + // If the `messageConfiguration` has updated, update the current `UIView`s arrangedSubviewsExcludingSpace .compactMap { $0 as? MessageConfigurable } - .forEach { $0.set(configuration: messageConfiguation) } + .forEach { $0.set(configuration: messageConfiguration) } } } /// Order of the posted `UIView`s in the `arrangedSubviews` /// - /// When `.default`, `spaceView` will be the first `arrangedSubview`. - /// When `.reversed`, `spaceView` will be the last `arrangedSubview`. - public var order: Order = .default { + /// When `.topToBottom`, `spaceView` will be the first `arrangedSubview`. + /// When `.bottomToTop`, `spaceView` will be the last `arrangedSubview`. + public var order: Order = .topToBottom { didSet { updateSpaceView(updateArrangedSubviews: true) } @@ -215,9 +203,9 @@ open class MessageStackView: UIStackView, Poster { let next: UIView? switch order { - case .default: + case .topToBottom: next = arrangedSubviews.elementAfterFirst(of: spaceView) - case .reversed: + case .bottomToTop: next = arrangedSubviews.elementBeforeFirst(of: spaceView) } @@ -233,17 +221,6 @@ open class MessageStackView: UIStackView, Poster { } } - // MARK: - Order - - /// Update `order` given `layout` - /// - Parameter layout: `MessageLayout` - public func updateOrderForLayout(_ layout: MessageLayout) { - switch layout { - case .top: order = .default - case .bottom: order = .reversed - } - } - // MARK: - Post /// `postArrangedSubview(view:order:)` with `view` and `order` @@ -260,9 +237,9 @@ open class MessageStackView: UIStackView, Poster { /// - order: `Order` private func postArrangedSubview(view: UIView, order: Order) { switch order { - case .default: + case .topToBottom: addArrangedSubview(view) - case .reversed: + case .bottomToTop: insertArrangedSubview(view, at: 0) } } @@ -296,7 +273,7 @@ extension MessageStackView: UIViewPoster { completion: @escaping () -> Void ) { postArrangedSubview(view: view) - (view as? MessageConfigurable)?.set(configuration: messageConfiguation) + (view as? MessageConfigurable)?.set(configuration: messageConfiguration) // Update spaceView here too incase properties on adjacent // arrangedSubview has, since posting, changed @@ -327,16 +304,3 @@ extension MessageStackView: UIViewPoster { } } } - -// MARK: - Order + Extensions - -private extension MessageStackView.Order { - - /// Other `Order` (opposite direction) - var switched: Self { - switch self { - case .default: return .reversed - case .reversed: return .default - } - } -} From 2994f5fc986ba4e62948ea2154452c3a0827c89b Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 16:21:13 +0100 Subject: [PATCH 05/15] Add `order` property to `PostView` --- Sources/Views/PostView/PostView.swift | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Sources/Views/PostView/PostView.swift b/Sources/Views/PostView/PostView.swift index 664c56a..aee1918 100644 --- a/Sources/Views/PostView/PostView.swift +++ b/Sources/Views/PostView/PostView.swift @@ -35,6 +35,14 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { /// `postManager` public var removeFromSuperviewOnEmpty = false + /// Define how the translation animation moves the subview + /// + /// When `.topToBottom`, the subview will be animated from the top of the screen down + /// When `.bottomToTop`, the subview will be animated from the bottom of the screen up + open var order: Order { + return .topToBottom + } + // MARK: - Init public convenience init() { @@ -126,10 +134,16 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { /// /// - Parameter view: `UIView` private func hiddenTranslationY(for view: UIView) -> CGAffineTransform { - return CGAffineTransform( - translationX: 0, - y: -(view.bounds.size.height + edgeInsets.top) - ) + let y: CGFloat + + switch order { + case .topToBottom: + y = -(view.bounds.size.height + edgeInsets.top) + case .bottomToTop: + y = view.bounds.size.height + edgeInsets.bottom + } + + return CGAffineTransform(translationX: 0, y: y) } // MARK: - Subview From 2b36bab24f56a8df3546f8c1b8d430e685a90167 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 16:25:28 +0100 Subject: [PATCH 06/15] Update docs in `PostView` --- Sources/Views/PostView/PostView.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/Views/PostView/PostView.swift b/Sources/Views/PostView/PostView.swift index aee1918..5cdc5c7 100644 --- a/Sources/Views/PostView/PostView.swift +++ b/Sources/Views/PostView/PostView.swift @@ -114,7 +114,9 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { // MARK: - Transform - /// Consider "hidden" views as transformed out of the bounds above. + /// Consider "hidden" views as transformed out of the bounds above when `order` is + /// `.topToBottom`, otherwise below when `order` is `.bottomToTop`. + /// /// With `clipsToBounds = true` these views will not be visible. /// We can then "show" them by animating their transform back to `.identity` /// @@ -126,6 +128,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { } /// `CGAffineTransform` to effectively "hide" a view off the top of `self`'s `bounds` + /// when `order` is `.topToBottom`, otherwise off the bottom when `order` is `.bottomToTop`. /// /// - Note: /// From the docs of `frame`: @@ -149,6 +152,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { // MARK: - Subview /// Add posted `subview`, constraining accordingly and ensuring `transform` for animation + /// /// - Parameter subview: `UIView` private func addPostSubview(_ subview: UIView) { addSubview(subview) @@ -159,6 +163,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { } /// Remove a previously posted `subview` and ensure layout + /// /// - Parameter subview: `UIView` private func removePostSubview(_ subview: UIView) { subview.removeFromSuperview() @@ -218,6 +223,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { } /// Called when a `view` was posted + /// /// - Parameters: /// - postManager: `PostManager` /// - view: The `UIView` that was posted @@ -228,6 +234,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { } /// Called when a `view` will be removed + /// /// - Parameters: /// - postManager: `PostManager` /// - view: The `UIView` that will be removed From 79c7111f4f57d01d6c46cf997b99c5fb8408f3dd Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 16:58:06 +0100 Subject: [PATCH 07/15] Create `Toast` with example --- .../RootTableViewController.swift | 3 +- .../ViewControllers/ToastViewController.swift | 37 ++++++++++++++ MessageStackView.xcodeproj/project.pbxproj | 8 +++ Sources/PostManager/PostGestureManager.swift | 31 ++++++++--- Sources/PostManager/PostManager.swift | 5 +- Sources/Views/PostView/PostView.swift | 2 +- Sources/Views/PostView/Toast.swift | 51 +++++++++++++++++++ 7 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 Example/ViewControllers/ToastViewController.swift create mode 100644 Sources/Views/PostView/Toast.swift diff --git a/Example/ViewControllers/RootTableViewController.swift b/Example/ViewControllers/RootTableViewController.swift index b9989dd..c13597c 100644 --- a/Example/ViewControllers/RootTableViewController.swift +++ b/Example/ViewControllers/RootTableViewController.swift @@ -24,7 +24,8 @@ class RootTableViewController: UITableViewController { ("Badge Message", BadgeMessageViewController.self, false), ("Message", MessageViewController.self, false), ("No Internet", NoInternetTabBarController.self, true), - ("Shadow", ShadowViewController.self, false) + ("Shadow", ShadowViewController.self, false), + ("Toast", ToastViewController.self, false) ] // MARK: - ViewController lifecycle diff --git a/Example/ViewControllers/ToastViewController.swift b/Example/ViewControllers/ToastViewController.swift new file mode 100644 index 0000000..528f87f --- /dev/null +++ b/Example/ViewControllers/ToastViewController.swift @@ -0,0 +1,37 @@ +// +// ToastViewController.swift +// Example +// +// Created by Ben Shutt on 02/06/2021. +// Copyright © 2021 3 SIDED CUBE APP PRODUCTIONS LTD. All rights reserved. +// + +import Foundation +import UIKit +import MessageStackView + +class ToastViewController: UIViewController { + + /// `Toast` to post at the bottom of the screen + private lazy var toast = Toast() + + // MARK: - ViewController lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + toast.addTo( + view: view, + layout: .bottom, + constrainToSafeArea: true + ) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + DispatchQueue.main.asyncAfterNow(time: .seconds(1)) { [weak self] in + self?.toast.post(message: "This is a toast!") + } + } +} diff --git a/MessageStackView.xcodeproj/project.pbxproj b/MessageStackView.xcodeproj/project.pbxproj index a2baa79..6e4d85b 100644 --- a/MessageStackView.xcodeproj/project.pbxproj +++ b/MessageStackView.xcodeproj/project.pbxproj @@ -30,6 +30,8 @@ B81AEE5224FFFB2E0068CE23 /* ParentShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE5124FFFB2E0068CE23 /* ParentShadowLayer.swift */; }; B81AEE5425003F270068CE23 /* ShadowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE5325003F270068CE23 /* ShadowViewController.swift */; }; B83CACB12667D5CE008FB755 /* Order.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83CACB02667D5CE008FB755 /* Order.swift */; }; + B83CACB32667D97E008FB755 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83CACB22667D97E008FB755 /* Toast.swift */; }; + B83CACB52667DA8A008FB755 /* ToastViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83CACB42667DA8A008FB755 /* ToastViewController.swift */; }; B872BE8725A3874C0031D619 /* UIImageView+Hidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = B872BE8625A3874C0031D619 /* UIImageView+Hidden.swift */; }; B872BE9125A387570031D619 /* UILabel+Hidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = B872BE9025A387570031D619 /* UILabel+Hidden.swift */; }; B875C98925126EB200FA05B5 /* UIViewController+System.swift in Sources */ = {isa = PBXBuildFile; fileRef = B875C98825126EB200FA05B5 /* UIViewController+System.swift */; }; @@ -174,6 +176,8 @@ B81AEE5124FFFB2E0068CE23 /* ParentShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentShadowLayer.swift; sourceTree = ""; }; B81AEE5325003F270068CE23 /* ShadowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowViewController.swift; sourceTree = ""; }; B83CACB02667D5CE008FB755 /* Order.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Order.swift; sourceTree = ""; }; + B83CACB22667D97E008FB755 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = ""; }; + B83CACB42667DA8A008FB755 /* ToastViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewController.swift; sourceTree = ""; }; B872BE8625A3874C0031D619 /* UIImageView+Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Hidden.swift"; sourceTree = ""; }; B872BE9025A387570031D619 /* UILabel+Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Hidden.swift"; sourceTree = ""; }; B875C98825126EB200FA05B5 /* UIViewController+System.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+System.swift"; sourceTree = ""; }; @@ -371,6 +375,7 @@ B8BB087924C4BC46000E2E87 /* MessageViewController.swift */, B87CC99C24CC263E002B697C /* NoInternetTabBarController.swift */, B81AEE5325003F270068CE23 /* ShadowViewController.swift */, + B83CACB42667DA8A008FB755 /* ToastViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -598,6 +603,7 @@ OBJ_74 /* MessageStackView.swift */, OBJ_75 /* PostView.swift */, B8F1C76224D8198E007C2712 /* ApplicationPostView.swift */, + B83CACB22667D97E008FB755 /* Toast.swift */, ); path = PostView; sourceTree = ""; @@ -815,6 +821,7 @@ B8BB088424C4BC46000E2E87 /* WindowViewController.swift in Sources */, B8BB088724C4BC46000E2E87 /* MessageViewController.swift in Sources */, B8BB088224C4BC46000E2E87 /* RootTableViewController.swift in Sources */, + B83CACB52667DA8A008FB755 /* ToastViewController.swift in Sources */, B87CC99D24CC263E002B697C /* NoInternetTabBarController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -857,6 +864,7 @@ OBJ_99 /* DispatchQueue+Extensions.swift in Sources */, OBJ_100 /* String+Extensions.swift in Sources */, OBJ_101 /* UIApplication+StatusBar.swift in Sources */, + B83CACB32667D97E008FB755 /* Toast.swift in Sources */, OBJ_102 /* UIEdgeInsets+Extensions.swift in Sources */, OBJ_103 /* UIView+Animation.swift in Sources */, B8BCDA8C24CDB55B0067F26C /* UIViewController+Lifecycle.m in Sources */, diff --git a/Sources/PostManager/PostGestureManager.swift b/Sources/PostManager/PostGestureManager.swift index 4e3c10b..ae7d611 100644 --- a/Sources/PostManager/PostGestureManager.swift +++ b/Sources/PostManager/PostGestureManager.swift @@ -43,6 +43,12 @@ public class PostGestureManager { /// Map `UIView`s to their "pan to dismiss" `UIPanGestureRecognizer` private var panGestureMap = [UIView: UIPanGestureRecognizer]() + /// `Order` to configure which direction the user is allowed to pan. + /// + /// - When `.topToBottom`, the user can pan/swipe up to dismiss + /// - When `.bottomToTop`, the user can pan/swipe down to dismiss + var order: Order = .topToBottom + // MARK: - Deinit deinit { @@ -51,6 +57,7 @@ public class PostGestureManager { // MARK: - Invalidate + /// Invalidate maps func invalidate() { tapGestureMap.forEach { $0.key.removeGestureRecognizer($0.value) @@ -152,19 +159,15 @@ public class PostGestureManager { // Pan did update case .changed: - view.transform = CGAffineTransform( - translationX: 0, - y: min(0, sender.translation(in: view.superview).y) - ) + let ty = sender.translation(in: view.superview).y + let y = order.translationY(ty) + view.transform = CGAffineTransform(translationX: 0, y: y) return // Pan did finish case .ended: let translateY = view.transform.ty - let centerY = view.center.y - - let finalY = centerY + translateY - if finalY < 0 { + if abs(translateY) > view.bounds.size.height * 0.5 { requestRemove(view: view) return } @@ -220,3 +223,15 @@ private extension UIGestureRecognizer.State { } } } + +// MARK: - Order + Translation + +private extension Order { + + func translationY(_ ty: CGFloat) -> CGFloat { + switch self { + case .topToBottom: return min(0, ty) + case .bottomToTop: return max(0, ty) + } + } +} diff --git a/Sources/PostManager/PostManager.swift b/Sources/PostManager/PostManager.swift index 0ef416e..19bb1b2 100644 --- a/Sources/PostManager/PostManager.swift +++ b/Sources/PostManager/PostManager.swift @@ -157,7 +157,8 @@ public class PostManager { animated: postRequest.animated.contains(.onPost), completion: { self.completePost(postRequest: postRequest) - }) + } + ) } /// Invoke on the completion of `poster` posting a `view` of a `postRequest`. @@ -217,7 +218,7 @@ public class PostManager { ) } - /// Remove (unpost) a posted `view` invalidatating appropriate properties. + /// Remove (un-post) a posted `view` invalidating appropriate properties. /// - Parameters: /// - view: `UIView` to remove /// - animated: Should the removal be animated diff --git a/Sources/Views/PostView/PostView.swift b/Sources/Views/PostView/PostView.swift index 5cdc5c7..164c959 100644 --- a/Sources/Views/PostView/PostView.swift +++ b/Sources/Views/PostView/PostView.swift @@ -66,7 +66,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { // is the first instance to reference `postManager`, lazily // instantiating it, referencing `self`, which is being // de-initialized... - _ = self.postManager.isSerialQueue + self.postManager.gestureManager.order = order } // MARK: - IntrinsicContentSize diff --git a/Sources/Views/PostView/Toast.swift b/Sources/Views/PostView/Toast.swift new file mode 100644 index 0000000..7fab0e9 --- /dev/null +++ b/Sources/Views/PostView/Toast.swift @@ -0,0 +1,51 @@ +// +// Toast.swift +// MessageStackView +// +// Created by Ben Shutt on 02/06/2021. +// Copyright © 2021 3 SIDED CUBE APP PRODUCTIONS LTD. All rights reserved. +// + +import Foundation + +/// A `PostView` which looks like and behaves like a "Toast" on Android. +/// That is, a short message which pops in from the bottom of the screen to display a message. +open class Toast: PostView { + + // MARK: - Override + + override open var order: Order { + return .bottomToTop + } + + // MARK: - Post + + /// Post a `message` + /// + /// - Parameter message: `String` message to post + /// + /// - Returns: `MessageView` + @discardableResult + open func post( + message: String, + dismissAfter: TimeInterval? = .defaultMessageDismiss, + animated: PostAnimation = .default + ) -> MessageView { + let messageView = post( + message: Message(title: message), + dismissAfter: dismissAfter, + animated: animated + ) + + messageView.clipsToBounds = true + messageView.layer.cornerRadius = 5 + messageView.backgroundColor = .darkGray + messageView.titleLabel.textColor = .white + messageView.titleLabel.font = UIFont.systemFont( + ofSize: 16, + weight: .regular + ) + + return messageView + } +} From cfb2e2a5816c3a6b281f4c1743eb9ff70348390d Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 17:27:08 +0100 Subject: [PATCH 08/15] Wrap width of `Toast` subviews instead of constraining to leading and trailing --- .../ViewControllers/ToastViewController.swift | 20 +++++++++++++--- Sources/Views/PostView/PostView.swift | 9 ++++++- Sources/Views/PostView/Toast.swift | 24 +++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/Example/ViewControllers/ToastViewController.swift b/Example/ViewControllers/ToastViewController.swift index 528f87f..ddc1d86 100644 --- a/Example/ViewControllers/ToastViewController.swift +++ b/Example/ViewControllers/ToastViewController.swift @@ -10,6 +10,7 @@ import Foundation import UIKit import MessageStackView +/// `UIViewController` to test `Toast` class ToastViewController: UIViewController { /// `Toast` to post at the bottom of the screen @@ -30,8 +31,21 @@ class ToastViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - DispatchQueue.main.asyncAfterNow(time: .seconds(1)) { [weak self] in - self?.toast.post(message: "This is a toast!") - } + toast.post(message: .shortMessage) + toast.post(message: .longMessage) } } + +// MARK: - String + Text + +private extension String { + + static let shortMessage = """ + This is a toast!" + """ + + static let longMessage = """ + This is a long toast to test how the text wraps when the width \ + of the text is greater than the width of the screen + """ +} diff --git a/Sources/Views/PostView/PostView.swift b/Sources/Views/PostView/PostView.swift index 164c959..c79fa7b 100644 --- a/Sources/Views/PostView/PostView.swift +++ b/Sources/Views/PostView/PostView.swift @@ -156,12 +156,19 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { /// - Parameter subview: `UIView` private func addPostSubview(_ subview: UIView) { addSubview(subview) - subview.edgeConstraints(to: self, insets: edgeInsets) + constrain(subview: subview) layoutIfNeeded() setTransform(on: subview, forHidden: true) } + /// Constrain the given `subview` + /// + /// - Parameter subview: `UIView` + func constrain(subview: UIView) { + subview.edgeConstraints(to: self, insets: edgeInsets) + } + /// Remove a previously posted `subview` and ensure layout /// /// - Parameter subview: `UIView` diff --git a/Sources/Views/PostView/Toast.swift b/Sources/Views/PostView/Toast.swift index 7fab0e9..0f915d1 100644 --- a/Sources/Views/PostView/Toast.swift +++ b/Sources/Views/PostView/Toast.swift @@ -18,6 +18,30 @@ open class Toast: PostView { return .bottomToTop } + /// Constrain the given `subview` + /// + /// - Parameter subview: `UIView` + override func constrain(subview: UIView) { + subview.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + subview.topAnchor.constraint( + equalTo: topAnchor, + constant: edgeInsets.top + ), + subview.bottomAnchor.constraint( + equalTo: bottomAnchor, + constant: -edgeInsets.bottom + ), + subview.centerXAnchor.constraint( + equalTo: centerXAnchor + ), + subview.widthAnchor.constraint( + lessThanOrEqualTo: widthAnchor, + constant: -(edgeInsets.left + edgeInsets.right) + ) + ]) + } + // MARK: - Post /// Post a `message` From fdbedd8000c14ffc492463320a98cdf03a2a3cd0 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 17:28:27 +0100 Subject: [PATCH 09/15] Update `Toast` color --- Example/ViewControllers/ToastViewController.swift | 2 +- Sources/Views/PostView/Toast.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/ViewControllers/ToastViewController.swift b/Example/ViewControllers/ToastViewController.swift index ddc1d86..03fd9c5 100644 --- a/Example/ViewControllers/ToastViewController.swift +++ b/Example/ViewControllers/ToastViewController.swift @@ -41,7 +41,7 @@ class ToastViewController: UIViewController { private extension String { static let shortMessage = """ - This is a toast!" + This is a toast! """ static let longMessage = """ diff --git a/Sources/Views/PostView/Toast.swift b/Sources/Views/PostView/Toast.swift index 0f915d1..a1db7d6 100644 --- a/Sources/Views/PostView/Toast.swift +++ b/Sources/Views/PostView/Toast.swift @@ -63,7 +63,7 @@ open class Toast: PostView { messageView.clipsToBounds = true messageView.layer.cornerRadius = 5 - messageView.backgroundColor = .darkGray + messageView.backgroundColor = .themeDarkGray messageView.titleLabel.textColor = .white messageView.titleLabel.font = UIFont.systemFont( ofSize: 16, From 275c49848016e015e7d16aec964c6e3659d526af Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 17:35:29 +0100 Subject: [PATCH 10/15] Fix typo in `PostGestureManager` --- Sources/PostManager/PostGestureManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/PostManager/PostGestureManager.swift b/Sources/PostManager/PostGestureManager.swift index ae7d611..87453a4 100644 --- a/Sources/PostManager/PostGestureManager.swift +++ b/Sources/PostManager/PostGestureManager.swift @@ -30,7 +30,7 @@ protocol PostGestureManagerDelegate: AnyObject { /// Manage gestures for posted `UIView`s to trigger a removal of that posted `UIView`. /// -/// E.g. The user pans a view off the top wanting for it to be dimissed. +/// E.g. The user pans a view off of the top wanting for it to be dismissed. /// E.g. T user tapped to dismiss public class PostGestureManager { From 6978ab862cfc9304e46560c268e4b80ef1fc88a1 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 17:48:16 +0100 Subject: [PATCH 11/15] Add `postIfNotShowing(message:dismissAfter:animated:)` function to `Toast` --- Sources/Views/PostView/Toast.swift | 37 +++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Sources/Views/PostView/Toast.swift b/Sources/Views/PostView/Toast.swift index a1db7d6..739f373 100644 --- a/Sources/Views/PostView/Toast.swift +++ b/Sources/Views/PostView/Toast.swift @@ -44,9 +44,44 @@ open class Toast: PostView { // MARK: - Post + /// Is the given `message` already being shown + /// + /// - Parameter message: `String` message to show + private func isShowing(message: String) -> Bool { + return postManager.currentPostRequests + .compactMap { $0.view as? MessageView } + .compactMap { $0.titleLabel.text } + .contains(message) + } + + /// Post a `message` if that message isn't already been shown + /// + /// - Parameters: + /// - message: `String` message to post + /// - dismissAfter: `TimeInterval` + /// - animated: `PostAnimation` + /// + /// - Returns: `MessageView` + @discardableResult + open func postIfNotShowing( + message: String, + dismissAfter: TimeInterval? = .defaultMessageDismiss, + animated: PostAnimation = .default + ) -> MessageView? { + guard !isShowing(message: message) else { return nil } + return post( + message: message, + dismissAfter: dismissAfter, + animated: animated + ) + } + /// Post a `message` /// - /// - Parameter message: `String` message to post + /// - Parameters: + /// - message: `String` message to post + /// - dismissAfter: `TimeInterval` + /// - animated: `PostAnimation` /// /// - Returns: `MessageView` @discardableResult From bdbb4c9790d849e206759f6c687be56ebb62d302 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 17:49:04 +0100 Subject: [PATCH 12/15] Update access control of `isShowing(message:)` in `Toast` --- Sources/Views/PostView/Toast.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Views/PostView/Toast.swift b/Sources/Views/PostView/Toast.swift index 739f373..cd04b0b 100644 --- a/Sources/Views/PostView/Toast.swift +++ b/Sources/Views/PostView/Toast.swift @@ -47,7 +47,7 @@ open class Toast: PostView { /// Is the given `message` already being shown /// /// - Parameter message: `String` message to show - private func isShowing(message: String) -> Bool { + open func isShowing(message: String) -> Bool { return postManager.currentPostRequests .compactMap { $0.view as? MessageView } .compactMap { $0.titleLabel.text } From 6d9d898248a88ed54c5b195b729e3c942dd310db Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Wed, 2 Jun 2021 18:52:07 +0100 Subject: [PATCH 13/15] Add `postIfNotShowing` check to `ToastViewController` --- Example/ViewControllers/ToastViewController.swift | 1 + Sources/Views/PostView/Toast.swift | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/ViewControllers/ToastViewController.swift b/Example/ViewControllers/ToastViewController.swift index 03fd9c5..dcd4355 100644 --- a/Example/ViewControllers/ToastViewController.swift +++ b/Example/ViewControllers/ToastViewController.swift @@ -32,6 +32,7 @@ class ToastViewController: UIViewController { super.viewDidAppear(animated) toast.post(message: .shortMessage) + toast.postIfNotShowing(message: .shortMessage) // Shouldn't show toast.post(message: .longMessage) } } diff --git a/Sources/Views/PostView/Toast.swift b/Sources/Views/PostView/Toast.swift index cd04b0b..3f72e29 100644 --- a/Sources/Views/PostView/Toast.swift +++ b/Sources/Views/PostView/Toast.swift @@ -49,8 +49,7 @@ open class Toast: PostView { /// - Parameter message: `String` message to show open func isShowing(message: String) -> Bool { return postManager.currentPostRequests - .compactMap { $0.view as? MessageView } - .compactMap { $0.titleLabel.text } + .compactMap { ($0.view as? MessageView)?.titleLabel.text } .contains(message) } From fc1f9b0aa5651ac814993d776d05a3d9849f1a2b Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 3 Jun 2021 13:17:16 +0100 Subject: [PATCH 14/15] Update `PostView` and `Toast` to constrain relative to safeAreaLayoutGuide and allow clicks outside of subviews to pass through --- .../ViewControllers/ToastViewController.swift | 60 +++++++++++++++++-- MessageStackView.xcodeproj/project.pbxproj | 22 +++++-- Sources/Constraints/EdgeConstraints.swift | 18 ++++-- Sources/Constraints/EdgeLayoutGuide.swift | 33 ++++++++++ Sources/Constraints/MessageLayout.swift | 6 +- .../{ => UIView}/UIView+Animation.swift | 0 .../{ => UIView}/UIView+Extensions.swift | 0 .../{ => UIView}/UIView+SafeArea.swift | 0 .../{ => UIView}/UIView+Shadow.swift | 0 .../UIView+ShadowComponents.swift | 0 Sources/Views/PostView/PostView.swift | 37 ++++++++++-- Sources/Views/PostView/Toast.swift | 25 +++++++- 12 files changed, 176 insertions(+), 25 deletions(-) create mode 100644 Sources/Constraints/EdgeLayoutGuide.swift rename Sources/Extensions/{ => UIView}/UIView+Animation.swift (100%) rename Sources/Extensions/{ => UIView}/UIView+Extensions.swift (100%) rename Sources/Extensions/{ => UIView}/UIView+SafeArea.swift (100%) rename Sources/Extensions/{ => UIView}/UIView+Shadow.swift (100%) rename Sources/Extensions/{ => UIView}/UIView+ShadowComponents.swift (100%) diff --git a/Example/ViewControllers/ToastViewController.swift b/Example/ViewControllers/ToastViewController.swift index dcd4355..0001025 100644 --- a/Example/ViewControllers/ToastViewController.swift +++ b/Example/ViewControllers/ToastViewController.swift @@ -16,16 +16,26 @@ class ToastViewController: UIViewController { /// `Toast` to post at the bottom of the screen private lazy var toast = Toast() + /// `UIButton` to add to bottom to make sure it can be clicked when the toast is slowing + private lazy var button: UIButton = { + let button = UIButton() + button.setTitle(.buttonTitleClick, for: .normal) + button.setTitleColor(.systemBlue, for: .normal) + button.addTarget( + self, + action: #selector(buttonTouchUpInside), + for: .touchUpInside + ) + return button + }() + // MARK: - ViewController lifecycle override func viewDidLoad() { super.viewDidLoad() - toast.addTo( - view: view, - layout: .bottom, - constrainToSafeArea: true - ) + addButtonToBottomLeading() + addAndConstrain(toast) } override func viewDidAppear(_ animated: Bool) { @@ -35,12 +45,52 @@ class ToastViewController: UIViewController { toast.postIfNotShowing(message: .shortMessage) // Shouldn't show toast.post(message: .longMessage) } + + // MARK: - Subviews + + private func addButtonToBottomLeading() { + view.addSubview(button) + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25), + button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -15) + ]) + } + + // MARK: - Actions + + /// `sender` received `.touchUpInside` `UIControl.Event` + /// + /// - Parameter sender: `UIButton` that invoked the action + @objc + private func buttonTouchUpInside(_ sender: UIButton) { + UIView.animate( + withDuration: 0.5, + delay: 0, + options: [.autoreverse], + animations: { + sender.setTitle(.buttonTitleClicked, for: .normal) + sender.transform = CGAffineTransform(scaleX: 2, y: 2) + }, completion: { _ in + sender.transform = .identity + sender.setTitle(.buttonTitleClick, for: .normal) + } + ) + } } // MARK: - String + Text private extension String { + static let buttonTitleClick = """ + Click Me + """ + + static let buttonTitleClicked = """ + Clicked! + """ + static let shortMessage = """ This is a toast! """ diff --git a/MessageStackView.xcodeproj/project.pbxproj b/MessageStackView.xcodeproj/project.pbxproj index 6e4d85b..a14c4f5 100644 --- a/MessageStackView.xcodeproj/project.pbxproj +++ b/MessageStackView.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ B81AEE5024FFF27A0068CE23 /* ShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE4F24FFF27A0068CE23 /* ShadowLayer.swift */; }; B81AEE5224FFFB2E0068CE23 /* ParentShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE5124FFFB2E0068CE23 /* ParentShadowLayer.swift */; }; B81AEE5425003F270068CE23 /* ShadowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE5325003F270068CE23 /* ShadowViewController.swift */; }; + B82405672668F6A200E4178B /* EdgeLayoutGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82405662668F6A200E4178B /* EdgeLayoutGuide.swift */; }; B83CACB12667D5CE008FB755 /* Order.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83CACB02667D5CE008FB755 /* Order.swift */; }; B83CACB32667D97E008FB755 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83CACB22667D97E008FB755 /* Toast.swift */; }; B83CACB52667DA8A008FB755 /* ToastViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83CACB42667DA8A008FB755 /* ToastViewController.swift */; }; @@ -175,6 +176,7 @@ B81AEE4F24FFF27A0068CE23 /* ShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowLayer.swift; sourceTree = ""; }; B81AEE5124FFFB2E0068CE23 /* ParentShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentShadowLayer.swift; sourceTree = ""; }; B81AEE5325003F270068CE23 /* ShadowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowViewController.swift; sourceTree = ""; }; + B82405662668F6A200E4178B /* EdgeLayoutGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeLayoutGuide.swift; sourceTree = ""; }; B83CACB02667D5CE008FB755 /* Order.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Order.swift; sourceTree = ""; }; B83CACB22667D97E008FB755 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = ""; }; B83CACB42667DA8A008FB755 /* ToastViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewController.swift; sourceTree = ""; }; @@ -319,6 +321,18 @@ path = Shadow; sourceTree = ""; }; + B82405682668F8EB00E4178B /* UIView */ = { + isa = PBXGroup; + children = ( + OBJ_25 /* UIView+Animation.swift */, + OBJ_26 /* UIView+Extensions.swift */, + OBJ_28 /* UIView+SafeArea.swift */, + OBJ_29 /* UIView+Shadow.swift */, + OBJ_30 /* UIView+ShadowComponents.swift */, + ); + path = UIView; + sourceTree = ""; + }; B872BE8525A3873F0031D619 /* Hidden */ = { isa = PBXGroup; children = ( @@ -421,6 +435,7 @@ OBJ_13 /* CenterConstraints.swift */, OBJ_14 /* Constrainable.swift */, OBJ_15 /* EdgeConstraints.swift */, + B82405662668F6A200E4178B /* EdgeLayoutGuide.swift */, OBJ_16 /* MessageLayout.swift */, OBJ_17 /* SizeConstraints.swift */, ); @@ -430,6 +445,7 @@ OBJ_18 /* Extensions */ = { isa = PBXGroup; children = ( + B82405682668F8EB00E4178B /* UIView */, B872BE8525A3873F0031D619 /* Hidden */, OBJ_19 /* CALayer+Animation.swift */, OBJ_20 /* CGRect+Extensions.swift */, @@ -437,11 +453,6 @@ OBJ_22 /* String+Extensions.swift */, OBJ_23 /* UIApplication+StatusBar.swift */, OBJ_24 /* UIEdgeInsets+Extensions.swift */, - OBJ_25 /* UIView+Animation.swift */, - OBJ_26 /* UIView+Extensions.swift */, - OBJ_28 /* UIView+SafeArea.swift */, - OBJ_29 /* UIView+Shadow.swift */, - OBJ_30 /* UIView+ShadowComponents.swift */, B87CC94B24C9F5A3002B697C /* Array+Extensions.swift */, B8BCDA8424CD8EB10067F26C /* UIApplication+KeyWindow.swift */, B81AEE4B24FFEE520068CE23 /* FloatingPoint+Equals.swift */, @@ -888,6 +899,7 @@ B83CACB12667D5CE008FB755 /* Order.swift in Sources */, OBJ_115 /* BadgeMessageView.swift in Sources */, OBJ_116 /* BadgeMessageViewable.swift in Sources */, + B82405672668F6A200E4178B /* EdgeLayoutGuide.swift in Sources */, OBJ_117 /* Poster+BadgeMessage.swift in Sources */, OBJ_118 /* Message.swift in Sources */, B81AEE4E24FFF2650068CE23 /* ShadowView.swift in Sources */, diff --git a/Sources/Constraints/EdgeConstraints.swift b/Sources/Constraints/EdgeConstraints.swift index a54922d..bd68116 100644 --- a/Sources/Constraints/EdgeConstraints.swift +++ b/Sources/Constraints/EdgeConstraints.swift @@ -64,26 +64,32 @@ struct EdgeConstraints: Constrainable { extension UIView { - /// Construct `EdgeConstraints` `NSLayoutConstraint`s from - /// `self` to `view` + /// Construct `EdgeConstraints` `NSLayoutConstraint`s from `self` to `view` /// /// - Parameters: /// - view: `UIView` to constrain to + /// - insets: `UIEdgeInsets` to inset + /// - safeAreaLayoutGuide: `Bool` constrain to `view`'s `safeAreaLayoutGuide` /// - activate: Activate the `NSLayoutConstraint`s @discardableResult func edgeConstraints( to view: UIView, insets: UIEdgeInsets = .zero, + safeAreaLayoutGuide: Bool = false, activate: Bool = true ) -> EdgeConstraints { translatesAutoresizingMaskIntoConstraints = false + // `EdgeLayoutGuide` of `view` to constrain `self` to + let viewLayoutGuide: EdgeLayoutGuide = + safeAreaLayoutGuide ? view.safeAreaLayoutGuide : view + // Create `NSLayoutConstraint`s to the edge anchors var edgeConstraints = EdgeConstraints( - leading: leadingAnchor.constraint(equalTo: view.leadingAnchor), - top: topAnchor.constraint(equalTo: view.topAnchor), - trailing: trailingAnchor.constraint(equalTo: view.trailingAnchor), - bottom: bottomAnchor.constraint(equalTo: view.bottomAnchor) + leading: leadingAnchor.constraint(equalTo: viewLayoutGuide.leadingAnchor), + top: topAnchor.constraint(equalTo: viewLayoutGuide.topAnchor), + trailing: trailingAnchor.constraint(equalTo: viewLayoutGuide.trailingAnchor), + bottom: bottomAnchor.constraint(equalTo: viewLayoutGuide.bottomAnchor) ) // Set `UIEdgeInsets` as provided diff --git a/Sources/Constraints/EdgeLayoutGuide.swift b/Sources/Constraints/EdgeLayoutGuide.swift new file mode 100644 index 0000000..19ddf73 --- /dev/null +++ b/Sources/Constraints/EdgeLayoutGuide.swift @@ -0,0 +1,33 @@ +// +// LayoutGuide.swift +// MessageStackView +// +// Created by Ben Shutt on 03/06/2021. +// Copyright © 2021 3 SIDED CUBE APP PRODUCTIONS LTD. All rights reserved. +// + +import Foundation + +/// Edge layout guide +protocol EdgeLayoutGuide { + + /// Leading anchor in X + var leadingAnchor: NSLayoutXAxisAnchor { get } + + /// Top anchor in Y + var topAnchor: NSLayoutYAxisAnchor { get } + + /// Trailing anchor in X + var trailingAnchor: NSLayoutXAxisAnchor { get } + + /// Bottom anchor in Y + var bottomAnchor: NSLayoutYAxisAnchor { get } +} + +// MARK: - UIView + EdgeLayoutGuide + +extension UIView: EdgeLayoutGuide {} + +// MARK: - UILayoutGuide + EdgeLayoutGuide + +extension UILayoutGuide: EdgeLayoutGuide {} diff --git a/Sources/Constraints/MessageLayout.swift b/Sources/Constraints/MessageLayout.swift index b3a9757..47efc39 100644 --- a/Sources/Constraints/MessageLayout.swift +++ b/Sources/Constraints/MessageLayout.swift @@ -41,7 +41,7 @@ public extension MessageLayout { /// Constrain `subview` to `superview` based on `self` (`MessageLayout`). /// - Parameters: /// - subview: `UIView` to add as a subview to `superview` - /// - superview: `UIView` superivew of `subview` + /// - superview: `UIView` superview of `subview` /// - safeAnchors: Constrain to safe anchors func constrain( subview: UIView, @@ -113,7 +113,9 @@ public extension UIView { // Constrain `self` layout.constrain( - subview: self, to: view, safeAnchors: constrainToSafeArea + subview: self, + to: view, + safeAnchors: constrainToSafeArea ) // Trigger a layout cycle diff --git a/Sources/Extensions/UIView+Animation.swift b/Sources/Extensions/UIView/UIView+Animation.swift similarity index 100% rename from Sources/Extensions/UIView+Animation.swift rename to Sources/Extensions/UIView/UIView+Animation.swift diff --git a/Sources/Extensions/UIView+Extensions.swift b/Sources/Extensions/UIView/UIView+Extensions.swift similarity index 100% rename from Sources/Extensions/UIView+Extensions.swift rename to Sources/Extensions/UIView/UIView+Extensions.swift diff --git a/Sources/Extensions/UIView+SafeArea.swift b/Sources/Extensions/UIView/UIView+SafeArea.swift similarity index 100% rename from Sources/Extensions/UIView+SafeArea.swift rename to Sources/Extensions/UIView/UIView+SafeArea.swift diff --git a/Sources/Extensions/UIView+Shadow.swift b/Sources/Extensions/UIView/UIView+Shadow.swift similarity index 100% rename from Sources/Extensions/UIView+Shadow.swift rename to Sources/Extensions/UIView/UIView+Shadow.swift diff --git a/Sources/Extensions/UIView+ShadowComponents.swift b/Sources/Extensions/UIView/UIView+ShadowComponents.swift similarity index 100% rename from Sources/Extensions/UIView+ShadowComponents.swift rename to Sources/Extensions/UIView/UIView+ShadowComponents.swift diff --git a/Sources/Views/PostView/PostView.swift b/Sources/Views/PostView/PostView.swift index c79fa7b..9433916 100644 --- a/Sources/Views/PostView/PostView.swift +++ b/Sources/Views/PostView/PostView.swift @@ -92,6 +92,8 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { animated: Bool, completion: @escaping () -> Void ) { + view.layoutIfNeeded() + guard animated else { setTransform(on: view, forHidden: hidden) completion() @@ -131,19 +133,22 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { /// when `order` is `.topToBottom`, otherwise off the bottom when `order` is `.bottomToTop`. /// /// - Note: - /// From the docs of `frame`: + /// Don't use `frame` in calculations here, from the docs of `frame`: /// "If the transform property is not the identity transform, the value of this property is undefined /// and therefore should be ignored." /// + /// In the docs of `safeAreaInsets`: + /// The insets only reflect only the portion of the view that is covered by the safe area. + /// /// - Parameter view: `UIView` private func hiddenTranslationY(for view: UIView) -> CGAffineTransform { let y: CGFloat switch order { case .topToBottom: - y = -(view.bounds.size.height + edgeInsets.top) + y = -(view.bounds.size.height + edgeInsets.top + safeAreaInsets.top) case .bottomToTop: - y = view.bounds.size.height + edgeInsets.bottom + y = view.bounds.size.height + edgeInsets.bottom + safeAreaInsets.bottom } return CGAffineTransform(translationX: 0, y: y) @@ -166,7 +171,11 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { /// /// - Parameter subview: `UIView` func constrain(subview: UIView) { - subview.edgeConstraints(to: self, insets: edgeInsets) + subview.edgeConstraints( + to: self, + insets: edgeInsets, + safeAreaLayoutGuide: true + ) } /// Remove a previously posted `subview` and ensure layout @@ -259,4 +268,24 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { guard removeFromSuperviewOnEmpty, !postManager.isActive else { return } removeFromSuperview() } + + // MARK: - Touches + + /// Only catch touches on subviews, otherwise pass touch through view + /// + /// - Parameters: + /// - point: `CGPoint` + /// - event: `UIEvent` + /// + /// - Returns: `Bool` + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + for subview in subviews { + let point = convert(point, to: subview) + if subview.hitTest(point, with: event) != nil { + return true + } + } + + return false + } } diff --git a/Sources/Views/PostView/Toast.swift b/Sources/Views/PostView/Toast.swift index 3f72e29..1c4e6f0 100644 --- a/Sources/Views/PostView/Toast.swift +++ b/Sources/Views/PostView/Toast.swift @@ -18,18 +18,21 @@ open class Toast: PostView { return .bottomToTop } - /// Constrain the given `subview` + /// Constrain the given `subview`. + /// Instead of constraining the subviews leading and trailing to the parent (with insets), + /// allow the width to be defined by the intrinsic content size of the subview. + /// I.e. in Android, wrap_content instead of match_parent /// /// - Parameter subview: `UIView` override func constrain(subview: UIView) { subview.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ subview.topAnchor.constraint( - equalTo: topAnchor, + equalTo: safeAreaLayoutGuide.topAnchor, constant: edgeInsets.top ), subview.bottomAnchor.constraint( - equalTo: bottomAnchor, + equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -edgeInsets.bottom ), subview.centerXAnchor.constraint( @@ -107,3 +110,19 @@ open class Toast: PostView { return messageView } } + +// MARK: - UIViewController + Toast + +public extension UIViewController { + + /// Add and constrain the given `toast` + /// + /// - Parameter toast: `Toast` + func addAndConstrain(_ toast: Toast) { + toast.addTo( + view: view, + layout: .bottom, + constrainToSafeArea: false + ) + } +} From 15ad995d6248c6d28a2915c2fe0304180ac2219a Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 3 Jun 2021 13:24:30 +0100 Subject: [PATCH 15/15] Change adding toast function name --- Example/ViewControllers/ToastViewController.swift | 2 +- Sources/Views/PostView/Toast.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/ViewControllers/ToastViewController.swift b/Example/ViewControllers/ToastViewController.swift index 0001025..2734591 100644 --- a/Example/ViewControllers/ToastViewController.swift +++ b/Example/ViewControllers/ToastViewController.swift @@ -35,7 +35,7 @@ class ToastViewController: UIViewController { super.viewDidLoad() addButtonToBottomLeading() - addAndConstrain(toast) + addAndConstrainToast(toast) } override func viewDidAppear(_ animated: Bool) { diff --git a/Sources/Views/PostView/Toast.swift b/Sources/Views/PostView/Toast.swift index 1c4e6f0..11c9353 100644 --- a/Sources/Views/PostView/Toast.swift +++ b/Sources/Views/PostView/Toast.swift @@ -118,7 +118,7 @@ public extension UIViewController { /// Add and constrain the given `toast` /// /// - Parameter toast: `Toast` - func addAndConstrain(_ toast: Toast) { + func addAndConstrainToast(_ toast: Toast) { toast.addTo( view: view, layout: .bottom,