Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a Toast message which shows from the bottom. (Very much an iOS equivalent of an Android toast) #33

Merged
merged 13 commits into from
Aug 20, 2021
2 changes: 1 addition & 1 deletion Example/ViewControllers/MessageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion Example/ViewControllers/RootTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions Example/ViewControllers/ToastViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// 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

/// `UIViewController` to test `Toast`
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)

toast.post(message: .shortMessage)
toast.postIfNotShowing(message: .shortMessage) // Shouldn't show
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
"""
}
12 changes: 12 additions & 0 deletions MessageStackView.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
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 */; };
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 */; };
Expand Down Expand Up @@ -172,6 +175,9 @@
B81AEE4F24FFF27A0068CE23 /* ShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowLayer.swift; sourceTree = "<group>"; };
B81AEE5124FFFB2E0068CE23 /* ParentShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentShadowLayer.swift; sourceTree = "<group>"; };
B81AEE5325003F270068CE23 /* ShadowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowViewController.swift; sourceTree = "<group>"; };
B83CACB02667D5CE008FB755 /* Order.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Order.swift; sourceTree = "<group>"; };
B83CACB22667D97E008FB755 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = "<group>"; };
B83CACB42667DA8A008FB755 /* ToastViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewController.swift; sourceTree = "<group>"; };
B872BE8625A3874C0031D619 /* UIImageView+Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Hidden.swift"; sourceTree = "<group>"; };
B872BE9025A387570031D619 /* UILabel+Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Hidden.swift"; sourceTree = "<group>"; };
B875C98825126EB200FA05B5 /* UIViewController+System.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+System.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -369,6 +375,7 @@
B8BB087924C4BC46000E2E87 /* MessageViewController.swift */,
B87CC99C24CC263E002B697C /* NoInternetTabBarController.swift */,
B81AEE5325003F270068CE23 /* ShadowViewController.swift */,
B83CACB42667DA8A008FB755 /* ToastViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
Expand Down Expand Up @@ -519,6 +526,7 @@
OBJ_54 /* PostAnimation.swift */,
OBJ_55 /* Queue.swift */,
B80B92AA2527324400F97364 /* Vector3.swift */,
B83CACB02667D5CE008FB755 /* Order.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -595,6 +603,7 @@
OBJ_74 /* MessageStackView.swift */,
OBJ_75 /* PostView.swift */,
B8F1C76224D8198E007C2712 /* ApplicationPostView.swift */,
B83CACB22667D97E008FB755 /* Toast.swift */,
);
path = PostView;
sourceTree = "<group>";
Expand Down Expand Up @@ -812,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;
Expand Down Expand Up @@ -854,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 */,
Expand All @@ -874,6 +885,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 */,
Expand Down
13 changes: 13 additions & 0 deletions Sources/Constraints/MessageLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
35 changes: 35 additions & 0 deletions Sources/Models/Order.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
33 changes: 24 additions & 9 deletions Sources/PostManager/PostGestureManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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 {
Expand All @@ -51,6 +57,7 @@ public class PostGestureManager {

// MARK: - Invalidate

/// Invalidate maps
func invalidate() {
tapGestureMap.forEach {
$0.key.removeGestureRecognizer($0.value)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
}
}
13 changes: 7 additions & 6 deletions Sources/PostManager/PostManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Sources/PostManager/UIView+Poster.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// UIView+MessageStackView.swift
// UIView+Poster.swift
// MessageStackView
//
// Created by Ben Shutt on 03/07/2020.
Expand Down Expand Up @@ -48,7 +48,7 @@ public extension UIView {
layout: layout,
constrainToSafeArea: constrainToSafeArea
)
messageStackView.updateOrderForLayout(layout)
messageStackView.order = layout.toOrder()
return messageStackView
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}()
Expand Down
2 changes: 1 addition & 1 deletion Sources/Reachability/ConnectivityViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
Loading