832 lines
32 KiB
Swift
832 lines
32 KiB
Swift
//
|
||
// Toast.swift
|
||
// Toast-Swift
|
||
//
|
||
// Copyright (c) 2015-2024 Charles Scalesse.
|
||
//
|
||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||
// copy of this software and associated documentation files (the
|
||
// "Software"), to deal in the Software without restriction, including
|
||
// without limitation the rights to use, copy, modify, merge, publish,
|
||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||
// permit persons to whom the Software is furnished to do so, subject to
|
||
// the following conditions:
|
||
//
|
||
// The above copyright notice and this permission notice shall be included
|
||
// in all copies or substantial portions of the Software.
|
||
//
|
||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
|
||
import ObjectiveC
|
||
import UIKit
|
||
import Lottie
|
||
|
||
/**
|
||
Toast is a Swift extension that adds toast notifications to the `UIView` object class.
|
||
It is intended to be simple, lightweight, and easy to use. Most toast notifications
|
||
can be triggered with a single line of code.
|
||
|
||
The `makeToast` methods create a new view and then display it as toast.
|
||
|
||
The `showToast` methods display any view as toast.
|
||
|
||
*/
|
||
public extension UIView {
|
||
/**
|
||
Keys used for associated objects.
|
||
*/
|
||
private struct ToastKeys {
|
||
static var timer = malloc(1)
|
||
static var duration = malloc(1)
|
||
static var point = malloc(1)
|
||
static var completion = malloc(1)
|
||
static var activeToasts = malloc(1)
|
||
static var activityView = malloc(1)
|
||
static var queue = malloc(1)
|
||
static var maskView = malloc(1)
|
||
}
|
||
|
||
/**
|
||
Swift closures can't be directly associated with objects via the
|
||
Objective-C runtime, so the (ugly) solution is to wrap them in a
|
||
class that can be used with associated objects.
|
||
*/
|
||
private class ToastCompletionWrapper {
|
||
let completion: ((Bool) -> Void)?
|
||
|
||
init(_ completion: ((Bool) -> Void)?) {
|
||
self.completion = completion
|
||
}
|
||
}
|
||
|
||
private enum ToastError: Error {
|
||
case missingParameters
|
||
}
|
||
|
||
private var activeToasts: NSMutableArray {
|
||
if let activeToasts = objc_getAssociatedObject(self, &ToastKeys.activeToasts) as? NSMutableArray {
|
||
return activeToasts
|
||
} else {
|
||
let activeToasts = NSMutableArray()
|
||
objc_setAssociatedObject(self, &ToastKeys.activeToasts, activeToasts, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
return activeToasts
|
||
}
|
||
}
|
||
|
||
private var queue: NSMutableArray {
|
||
if let queue = objc_getAssociatedObject(self, &ToastKeys.queue) as? NSMutableArray {
|
||
return queue
|
||
} else {
|
||
let queue = NSMutableArray()
|
||
objc_setAssociatedObject(self, &ToastKeys.queue, queue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
return queue
|
||
}
|
||
}
|
||
|
||
// MARK: - Make Toast Methods
|
||
|
||
/**
|
||
Creates and presents a new toast view.
|
||
|
||
@param message The message to be displayed
|
||
@param duration The toast duration
|
||
@param position The toast's position
|
||
@param title The title
|
||
@param image The image
|
||
@param style The style. The shared style will be used when nil
|
||
@param completion The completion closure, executed after the toast view disappears.
|
||
didTap will be `true` if the toast view was dismissed from a tap.
|
||
*/
|
||
func makeToast(_ message: String?, duration: TimeInterval = ToastManager.shared.duration, position: ToastPosition = ToastManager.shared.position, title: String? = nil, image: UIImage? = nil, style: ToastStyle = ToastManager.shared.style, completion: ((_ didTap: Bool) -> Void)? = nil) {
|
||
do {
|
||
let toast = try toastViewForMessage(message, title: title, image: image, style: style)
|
||
showToast(toast, duration: duration, position: position, completion: completion)
|
||
} catch ToastError.missingParameters {
|
||
print("Error: message, title, and image are all nil")
|
||
} catch {}
|
||
}
|
||
|
||
/**
|
||
Creates a new toast view and presents it at a given center point.
|
||
|
||
@param message The message to be displayed
|
||
@param duration The toast duration
|
||
@param point The toast's center point
|
||
@param title The title
|
||
@param image The image
|
||
@param style The style. The shared style will be used when nil
|
||
@param completion The completion closure, executed after the toast view disappears.
|
||
didTap will be `true` if the toast view was dismissed from a tap.
|
||
*/
|
||
func makeToast(_ message: String?, duration: TimeInterval = ToastManager.shared.duration, point: CGPoint, title: String?, image: UIImage?, style: ToastStyle = ToastManager.shared.style, completion: ((_ didTap: Bool) -> Void)?) {
|
||
do {
|
||
let toast = try toastViewForMessage(message, title: title, image: image, style: style)
|
||
showToast(toast, duration: duration, point: point, completion: completion)
|
||
} catch ToastError.missingParameters {
|
||
print("Error: message, title, and image cannot all be nil")
|
||
} catch {}
|
||
}
|
||
|
||
// MARK: - Show Toast Methods
|
||
|
||
/**
|
||
Displays any view as toast at a provided position and duration. The completion closure
|
||
executes when the toast view completes. `didTap` will be `true` if the toast view was
|
||
dismissed from a tap.
|
||
|
||
@param toast The view to be displayed as toast
|
||
@param duration The notification duration
|
||
@param position The toast's position
|
||
@param completion The completion block, executed after the toast view disappears.
|
||
didTap will be `true` if the toast view was dismissed from a tap.
|
||
*/
|
||
func showToast(_ toast: UIView, duration: TimeInterval = ToastManager.shared.duration, position: ToastPosition = ToastManager.shared.position, completion: ((_ didTap: Bool) -> Void)? = nil) {
|
||
let point = position.centerPoint(forToast: toast, inSuperview: self)
|
||
showToast(toast, duration: duration, point: point, completion: completion)
|
||
}
|
||
|
||
/**
|
||
Displays any view as toast at a provided center point and duration. The completion closure
|
||
executes when the toast view completes. `didTap` will be `true` if the toast view was
|
||
dismissed from a tap.
|
||
|
||
@param toast The view to be displayed as toast
|
||
@param duration The notification duration
|
||
@param point The toast's center point
|
||
@param completion The completion block, executed after the toast view disappears.
|
||
didTap will be `true` if the toast view was dismissed from a tap.
|
||
*/
|
||
func showToast(_ toast: UIView, duration: TimeInterval = ToastManager.shared.duration, point: CGPoint, completion: ((_ didTap: Bool) -> Void)? = nil) {
|
||
objc_setAssociatedObject(toast, &ToastKeys.completion, ToastCompletionWrapper(completion), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
|
||
if ToastManager.shared.isQueueEnabled, activeToasts.count > 0 {
|
||
objc_setAssociatedObject(toast, &ToastKeys.duration, NSNumber(value: duration), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
objc_setAssociatedObject(toast, &ToastKeys.point, NSValue(cgPoint: point), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
|
||
queue.add(toast)
|
||
} else {
|
||
showToast(toast, duration: duration, point: point)
|
||
}
|
||
}
|
||
|
||
// MARK: - Hide Toast Methods
|
||
|
||
/**
|
||
Hides the active toast. If there are multiple toasts active in a view, this method
|
||
hides the oldest toast (the first of the toasts to have been presented).
|
||
|
||
@see `hideAllToasts()` to remove all active toasts from a view.
|
||
|
||
@warning This method has no effect on activity toasts. Use `hideToastActivity` to
|
||
hide activity toasts.
|
||
|
||
*/
|
||
func hideToast() {
|
||
guard let activeToast = activeToasts.firstObject as? UIView else { return }
|
||
hideToast(activeToast)
|
||
}
|
||
|
||
/**
|
||
Hides an active toast.
|
||
|
||
@param toast The active toast view to dismiss. Any toast that is currently being displayed
|
||
on the screen is considered active.
|
||
|
||
@warning this does not clear a toast view that is currently waiting in the queue.
|
||
*/
|
||
func hideToast(_ toast: UIView) {
|
||
guard activeToasts.contains(toast) else { return }
|
||
hideToast(toast, fromTap: false)
|
||
}
|
||
|
||
/**
|
||
Hides all toast views.
|
||
|
||
@param includeActivity If `true`, toast activity will also be hidden. Default is `false`.
|
||
@param clearQueue If `true`, removes all toast views from the queue. Default is `true`.
|
||
*/
|
||
func hideAllToasts(includeActivity: Bool = false, clearQueue: Bool = true) {
|
||
if clearQueue {
|
||
clearToastQueue()
|
||
}
|
||
|
||
activeToasts.compactMap { $0 as? UIView }
|
||
.forEach { hideToast($0) }
|
||
|
||
if includeActivity {
|
||
hideToastActivity()
|
||
}
|
||
}
|
||
|
||
/**
|
||
Removes all toast views from the queue. This has no effect on toast views that are
|
||
active. Use `hideAllToasts(clearQueue:)` to hide the active toasts views and clear
|
||
the queue.
|
||
*/
|
||
func clearToastQueue() {
|
||
queue.removeAllObjects()
|
||
}
|
||
|
||
// MARK: - Activity Methods
|
||
|
||
/**
|
||
Creates and displays a new toast activity indicator view at a specified position.
|
||
|
||
@warning Only one toast activity indicator view can be presented per superview. Subsequent
|
||
calls to `makeToastActivity(position:)` will be ignored until `hideToastActivity()` is called.
|
||
|
||
@warning `makeToastActivity(position:)` works independently of the `showToast` methods. Toast
|
||
activity views can be presented and dismissed while toast views are being displayed.
|
||
`makeToastActivity(position:)` has no effect on the queueing behavior of the `showToast` methods.
|
||
|
||
@param position The toast's position
|
||
@param blockUserInteraction Whether to block user interaction with other elements. Default is false.
|
||
*/
|
||
func makeToastActivity(_ position: ToastPosition = .center, blockUserInteraction: Bool = false) {
|
||
// sanity
|
||
guard objc_getAssociatedObject(self, &ToastKeys.activityView) as? UIView == nil else { return }
|
||
|
||
let toast = createToastActivityView()
|
||
let point = position.centerPoint(forToast: toast, inSuperview: self)
|
||
makeToastActivity(toast, point: point, blockUserInteraction: blockUserInteraction)
|
||
}
|
||
|
||
/**
|
||
Creates and displays a new toast activity indicator view at a specified position.
|
||
|
||
@warning Only one toast activity indicator view can be presented per superview. Subsequent
|
||
calls to `makeToastActivity(position:)` will be ignored until `hideToastActivity()` is called.
|
||
|
||
@warning `makeToastActivity(position:)` works independently of the `showToast` methods. Toast
|
||
activity views can be presented and dismissed while toast views are being displayed.
|
||
`makeToastActivity(position:)` has no effect on the queueing behavior of the `showToast` methods.
|
||
|
||
@param point The toast's center point
|
||
@param blockUserInteraction Whether to block user interaction with other elements. Default is false.
|
||
*/
|
||
func makeToastActivity(_ point: CGPoint, blockUserInteraction: Bool = false) {
|
||
// sanity
|
||
guard objc_getAssociatedObject(self, &ToastKeys.activityView) as? UIView == nil else { return }
|
||
|
||
let toast = createToastActivityView()
|
||
makeToastActivity(toast, point: point, blockUserInteraction: blockUserInteraction)
|
||
}
|
||
|
||
/**
|
||
Dismisses the active toast activity indicator view.
|
||
*/
|
||
func hideToastActivity() {
|
||
if let toast = objc_getAssociatedObject(self, &ToastKeys.activityView) as? UIView {
|
||
UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseIn, .beginFromCurrentState], animations: {
|
||
toast.alpha = 0.0
|
||
}) { _ in
|
||
toast.removeFromSuperview()
|
||
objc_setAssociatedObject(self, &ToastKeys.activityView, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
}
|
||
}
|
||
|
||
// 移除遮罩层
|
||
if let maskView = objc_getAssociatedObject(self, &ToastKeys.maskView) as? UIView {
|
||
UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseIn, .beginFromCurrentState], animations: {
|
||
maskView.alpha = 0.0
|
||
}) { _ in
|
||
maskView.removeFromSuperview()
|
||
objc_setAssociatedObject(self, &ToastKeys.maskView, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Helper Methods
|
||
|
||
/**
|
||
Returns `true` if a toast view or toast activity view is actively being displayed.
|
||
*/
|
||
func isShowingToast() -> Bool {
|
||
return activeToasts.count > 0 || objc_getAssociatedObject(self, &ToastKeys.activityView) != nil
|
||
}
|
||
|
||
// MARK: - Private Activity Methods
|
||
|
||
private func makeToastActivity(_ toast: UIView, point: CGPoint, blockUserInteraction: Bool = false) {
|
||
toast.alpha = 0.0
|
||
toast.center = point
|
||
|
||
objc_setAssociatedObject(self, &ToastKeys.activityView, toast, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
|
||
// 如果需要阻止用户交互,添加遮罩层
|
||
if blockUserInteraction {
|
||
let maskView = createMaskView()
|
||
objc_setAssociatedObject(self, &ToastKeys.maskView, maskView, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
addSubview(maskView)
|
||
maskView.addSubview(toast)
|
||
} else {
|
||
addSubview(toast)
|
||
}
|
||
|
||
UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: .curveEaseOut, animations: {
|
||
toast.alpha = 1.0
|
||
})
|
||
}
|
||
|
||
|
||
private func createToastActivityView() -> UIView {
|
||
let style = ToastManager.shared.style
|
||
|
||
let activityView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: style.activitySize.width, height: style.activitySize.height))
|
||
activityView.backgroundColor = style.activityBackgroundColor
|
||
activityView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
|
||
activityView.layer.cornerRadius = style.cornerRadius
|
||
|
||
if style.displayShadow {
|
||
activityView.layer.shadowColor = style.shadowColor.cgColor
|
||
activityView.layer.shadowOpacity = style.shadowOpacity
|
||
activityView.layer.shadowRadius = style.shadowRadius
|
||
activityView.layer.shadowOffset = style.shadowOffset
|
||
}
|
||
|
||
// let activityIndicatorView = UIActivityIndicatorView(style: .large)
|
||
// activityIndicatorView.center = CGPoint(x: activityView.bounds.size.width / 2.0, y: activityView.bounds.size.height / 2.0)
|
||
// activityView.addSubview(activityIndicatorView)
|
||
// activityIndicatorView.color = style.activityIndicatorColor
|
||
// activityIndicatorView.startAnimating()
|
||
|
||
let animation = LottieAnimation.named("single_ring")
|
||
let animationView = LottieAnimationView(animation: animation)
|
||
animationView.contentMode = .scaleAspectFit
|
||
animationView.loopMode = .loop
|
||
animationView.backgroundBehavior = .pauseAndRestore
|
||
let size = CGSize(width: 40, height: 40)
|
||
animationView.size = size
|
||
animationView.backgroundColor = .clear
|
||
activityView.addSubview(animationView)
|
||
animationView.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.size.equalTo(size)
|
||
}
|
||
animationView.play()
|
||
|
||
return activityView
|
||
}
|
||
|
||
private func createMaskView() -> UIView {
|
||
let maskView = UIView(frame: bounds)
|
||
maskView.backgroundColor = UIColor.clear
|
||
maskView.isUserInteractionEnabled = true
|
||
maskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||
return maskView
|
||
}
|
||
|
||
// MARK: - Private Show/Hide Methods
|
||
|
||
private func showToast(_ toast: UIView, duration: TimeInterval, point: CGPoint) {
|
||
toast.center = point
|
||
toast.alpha = 0.0
|
||
|
||
if ToastManager.shared.isTapToDismissEnabled {
|
||
let recognizer = UITapGestureRecognizer(target: self, action: #selector(UIView.handleToastTapped(_:)))
|
||
toast.addGestureRecognizer(recognizer)
|
||
toast.isUserInteractionEnabled = true
|
||
toast.isExclusiveTouch = true
|
||
}
|
||
|
||
activeToasts.add(toast)
|
||
addSubview(toast)
|
||
|
||
let timer = Timer(timeInterval: duration, target: self, selector: #selector(UIView.toastTimerDidFinish(_:)), userInfo: toast, repeats: false)
|
||
objc_setAssociatedObject(toast, &ToastKeys.timer, timer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
|
||
UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseOut, .allowUserInteraction], animations: {
|
||
toast.alpha = 1.0
|
||
}) { _ in
|
||
guard let timer = objc_getAssociatedObject(toast, &ToastKeys.timer) as? Timer else { return }
|
||
RunLoop.main.add(timer, forMode: .common)
|
||
}
|
||
|
||
UIAccessibility.post(notification: .screenChanged, argument: toast)
|
||
}
|
||
|
||
private func hideToast(_ toast: UIView, fromTap: Bool) {
|
||
if let timer = objc_getAssociatedObject(toast, &ToastKeys.timer) as? Timer {
|
||
timer.invalidate()
|
||
}
|
||
|
||
UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseIn, .beginFromCurrentState], animations: {
|
||
toast.alpha = 0.0
|
||
}) { _ in
|
||
toast.removeFromSuperview()
|
||
self.activeToasts.remove(toast)
|
||
|
||
if let wrapper = objc_getAssociatedObject(toast, &ToastKeys.completion) as? ToastCompletionWrapper, let completion = wrapper.completion {
|
||
completion(fromTap)
|
||
}
|
||
|
||
if let nextToast = self.queue.firstObject as? UIView, let duration = objc_getAssociatedObject(nextToast, &ToastKeys.duration) as? NSNumber, let point = objc_getAssociatedObject(nextToast, &ToastKeys.point) as? NSValue {
|
||
self.queue.removeObject(at: 0)
|
||
self.showToast(nextToast, duration: duration.doubleValue, point: point.cgPointValue)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Events
|
||
|
||
@objc
|
||
private func handleToastTapped(_ recognizer: UITapGestureRecognizer) {
|
||
guard let toast = recognizer.view else { return }
|
||
hideToast(toast, fromTap: true)
|
||
}
|
||
|
||
@objc
|
||
private func toastTimerDidFinish(_ timer: Timer) {
|
||
guard let toast = timer.userInfo as? UIView else { return }
|
||
hideToast(toast)
|
||
}
|
||
|
||
// MARK: - Toast Construction
|
||
|
||
/**
|
||
Creates a new toast view with any combination of message, title, and image.
|
||
The look and feel is configured via the style. Unlike the `makeToast` methods,
|
||
this method does not present the toast view automatically. One of the `showToast`
|
||
methods must be used to present the resulting view.
|
||
|
||
@warning if message, title, and image are all nil, this method will throw
|
||
`ToastError.missingParameters`
|
||
|
||
@param message The message to be displayed
|
||
@param title The title
|
||
@param image The image
|
||
@param style The style. The shared style will be used when nil
|
||
@throws `ToastError.missingParameters` when message, title, and image are all nil
|
||
@return The newly created toast view
|
||
*/
|
||
func toastViewForMessage(_ message: String?, title: String?, image: UIImage?, style: ToastStyle) throws -> UIView {
|
||
// sanity
|
||
guard message != nil || title != nil || image != nil else {
|
||
throw ToastError.missingParameters
|
||
}
|
||
|
||
var messageLabel: UILabel?
|
||
var titleLabel: UILabel?
|
||
var imageView: UIImageView?
|
||
|
||
let wrapperView = UIView()
|
||
wrapperView.backgroundColor = style.backgroundColor
|
||
wrapperView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
|
||
wrapperView.layer.cornerRadius = style.cornerRadius
|
||
|
||
if style.displayShadow {
|
||
wrapperView.layer.shadowColor = style.shadowColor.cgColor
|
||
wrapperView.layer.shadowOpacity = style.shadowOpacity
|
||
wrapperView.layer.shadowRadius = style.shadowRadius
|
||
wrapperView.layer.shadowOffset = style.shadowOffset
|
||
}
|
||
|
||
if let image = image {
|
||
imageView = UIImageView(image: image)
|
||
imageView?.contentMode = .scaleAspectFit
|
||
imageView?.frame = CGRect(x: style.horizontalPadding, y: style.verticalPadding, width: style.imageSize.width, height: style.imageSize.height)
|
||
}
|
||
|
||
var imageRect = CGRect.zero
|
||
|
||
if let imageView = imageView {
|
||
imageRect.origin.x = style.horizontalPadding
|
||
imageRect.origin.y = style.verticalPadding
|
||
imageRect.size.width = imageView.bounds.size.width
|
||
imageRect.size.height = imageView.bounds.size.height
|
||
}
|
||
|
||
if let title = title {
|
||
titleLabel = UILabel()
|
||
titleLabel?.numberOfLines = style.titleNumberOfLines
|
||
titleLabel?.font = style.titleFont
|
||
titleLabel?.textAlignment = style.titleAlignment
|
||
titleLabel?.lineBreakMode = .byTruncatingTail
|
||
titleLabel?.textColor = style.titleColor
|
||
titleLabel?.backgroundColor = UIColor.clear
|
||
titleLabel?.text = title
|
||
|
||
let maxTitleSize = CGSize(width: (bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: bounds.size.height * style.maxHeightPercentage)
|
||
let titleSize = titleLabel?.sizeThatFits(maxTitleSize)
|
||
if let titleSize = titleSize {
|
||
titleLabel?.frame = CGRect(x: 0.0, y: 0.0, width: titleSize.width, height: titleSize.height)
|
||
}
|
||
}
|
||
|
||
if let message = message {
|
||
messageLabel = UILabel()
|
||
messageLabel?.text = message
|
||
messageLabel?.numberOfLines = style.messageNumberOfLines
|
||
messageLabel?.font = style.messageFont
|
||
messageLabel?.textAlignment = style.messageAlignment
|
||
messageLabel?.lineBreakMode = .byTruncatingTail
|
||
messageLabel?.textColor = style.messageColor
|
||
messageLabel?.backgroundColor = UIColor.clear
|
||
|
||
let maxMessageSize = CGSize(width: (bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: bounds.size.height * style.maxHeightPercentage)
|
||
let messageSize = messageLabel?.sizeThatFits(maxMessageSize)
|
||
if let messageSize = messageSize {
|
||
let actualWidth = min(messageSize.width, maxMessageSize.width)
|
||
let actualHeight = min(messageSize.height, maxMessageSize.height)
|
||
messageLabel?.frame = CGRect(x: 0.0, y: 0.0, width: actualWidth, height: actualHeight)
|
||
}
|
||
}
|
||
|
||
var titleRect = CGRect.zero
|
||
|
||
if let titleLabel = titleLabel {
|
||
titleRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding
|
||
titleRect.origin.y = style.verticalPadding
|
||
titleRect.size.width = titleLabel.bounds.size.width
|
||
titleRect.size.height = titleLabel.bounds.size.height
|
||
}
|
||
|
||
var messageRect = CGRect.zero
|
||
|
||
if let messageLabel = messageLabel {
|
||
messageRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding
|
||
messageRect.origin.y = titleRect.origin.y + titleRect.size.height + style.verticalPadding
|
||
messageRect.size.width = messageLabel.bounds.size.width
|
||
messageRect.size.height = messageLabel.bounds.size.height
|
||
}
|
||
|
||
let longerWidth = max(titleRect.size.width, messageRect.size.width)
|
||
let longerX = max(titleRect.origin.x, messageRect.origin.x)
|
||
let wrapperWidth = max(imageRect.size.width + (style.horizontalPadding * 2.0), longerX + longerWidth + style.horizontalPadding)
|
||
|
||
let textMaxY = messageRect.size.height <= 0.0 && titleRect.size.height > 0.0 ? titleRect.maxY : messageRect.maxY
|
||
let wrapperHeight = max(textMaxY + style.verticalPadding, imageRect.size.height + (style.verticalPadding * 2.0))
|
||
|
||
wrapperView.frame = CGRect(x: 0.0, y: 0.0, width: wrapperWidth, height: wrapperHeight)
|
||
|
||
if let titleLabel = titleLabel {
|
||
titleRect.size.width = longerWidth
|
||
titleLabel.frame = titleRect
|
||
wrapperView.addSubview(titleLabel)
|
||
}
|
||
|
||
if let messageLabel = messageLabel {
|
||
messageRect.size.width = longerWidth
|
||
messageLabel.frame = messageRect
|
||
wrapperView.addSubview(messageLabel)
|
||
}
|
||
|
||
if let imageView = imageView {
|
||
wrapperView.addSubview(imageView)
|
||
}
|
||
|
||
return wrapperView
|
||
}
|
||
}
|
||
|
||
// MARK: - Toast Style
|
||
|
||
/**
|
||
`ToastStyle` instances define the look and feel for toast views created via the
|
||
`makeToast` methods as well for toast views created directly with
|
||
`toastViewForMessage(message:title:image:style:)`.
|
||
|
||
@warning `ToastStyle` offers relatively simple styling options for the default
|
||
toast view. If you require a toast view with more complex UI, it probably makes more
|
||
sense to create your own custom UIView subclass and present it with the `showToast`
|
||
methods.
|
||
*/
|
||
public struct ToastStyle {
|
||
public init() {}
|
||
|
||
/**
|
||
The background color. Default is `.black` at 80% opacity.
|
||
*/
|
||
public var backgroundColor: UIColor = UIColor.black.withAlphaComponent(0.8)
|
||
|
||
/**
|
||
The title color. Default is `UIColor.whiteColor()`.
|
||
*/
|
||
public var titleColor: UIColor = .white
|
||
|
||
/**
|
||
The message color. Default is `.white`.
|
||
*/
|
||
public var messageColor: UIColor = .white
|
||
|
||
/**
|
||
A percentage value from 0.0 to 1.0, representing the maximum width of the toast
|
||
view relative to it's superview. Default is 0.8 (80% of the superview's width).
|
||
*/
|
||
public var maxWidthPercentage: CGFloat = 0.8 {
|
||
didSet {
|
||
maxWidthPercentage = max(min(maxWidthPercentage, 1.0), 0.0)
|
||
}
|
||
}
|
||
|
||
/**
|
||
A percentage value from 0.0 to 1.0, representing the maximum height of the toast
|
||
view relative to it's superview. Default is 0.8 (80% of the superview's height).
|
||
*/
|
||
public var maxHeightPercentage: CGFloat = 0.8 {
|
||
didSet {
|
||
maxHeightPercentage = max(min(maxHeightPercentage, 1.0), 0.0)
|
||
}
|
||
}
|
||
|
||
/**
|
||
The spacing from the horizontal edge of the toast view to the content. When an image
|
||
is present, this is also used as the padding between the image and the text.
|
||
Default is 10.0.
|
||
|
||
*/
|
||
public var horizontalPadding: CGFloat = 10.0
|
||
|
||
/**
|
||
The spacing from the vertical edge of the toast view to the content. When a title
|
||
is present, this is also used as the padding between the title and the message.
|
||
Default is 10.0. On iOS11+, this value is added added to the `safeAreaInset.top`
|
||
and `safeAreaInsets.bottom`.
|
||
*/
|
||
public var verticalPadding: CGFloat = 10.0
|
||
|
||
/**
|
||
The corner radius. Default is 10.0.
|
||
*/
|
||
public var cornerRadius: CGFloat = 10.0
|
||
|
||
/**
|
||
The title font. Default is `.boldSystemFont(16.0)`.
|
||
*/
|
||
public var titleFont: UIFont = .boldSystemFont(ofSize: 16.0)
|
||
|
||
/**
|
||
The message font. Default is `.systemFont(ofSize: 16.0)`.
|
||
*/
|
||
public var messageFont: UIFont = .systemFont(ofSize: 16.0)
|
||
|
||
/**
|
||
The title text alignment. Default is `NSTextAlignment.Left`.
|
||
*/
|
||
public var titleAlignment: NSTextAlignment = .left
|
||
|
||
/**
|
||
The message text alignment. Default is `NSTextAlignment.Left`.
|
||
*/
|
||
public var messageAlignment: NSTextAlignment = .left
|
||
|
||
/**
|
||
The maximum number of lines for the title. The default is 0 (no limit).
|
||
*/
|
||
public var titleNumberOfLines = 0
|
||
|
||
/**
|
||
The maximum number of lines for the message. The default is 0 (no limit).
|
||
*/
|
||
public var messageNumberOfLines = 0
|
||
|
||
/**
|
||
Enable or disable a shadow on the toast view. Default is `false`.
|
||
*/
|
||
public var displayShadow = false
|
||
|
||
/**
|
||
The shadow color. Default is `.black`.
|
||
*/
|
||
public var shadowColor: UIColor = .black
|
||
|
||
/**
|
||
A value from 0.0 to 1.0, representing the opacity of the shadow.
|
||
Default is 0.8 (80% opacity).
|
||
*/
|
||
public var shadowOpacity: Float = 0.8 {
|
||
didSet {
|
||
shadowOpacity = max(min(shadowOpacity, 1.0), 0.0)
|
||
}
|
||
}
|
||
|
||
/**
|
||
The shadow radius. Default is 6.0.
|
||
*/
|
||
public var shadowRadius: CGFloat = 6.0
|
||
|
||
/**
|
||
The shadow offset. The default is 4 x 4.
|
||
*/
|
||
public var shadowOffset = CGSize(width: 4.0, height: 4.0)
|
||
|
||
/**
|
||
The image size. The default is 80 x 80.
|
||
*/
|
||
public var imageSize = CGSize(width: 80.0, height: 80.0)
|
||
|
||
/**
|
||
The size of the toast activity view when `makeToastActivity(position:)` is called.
|
||
Default is 100 x 100.
|
||
*/
|
||
public var activitySize = CGSize(width: 100.0, height: 100.0)
|
||
|
||
/**
|
||
The fade in/out animation duration. Default is 0.2.
|
||
*/
|
||
public var fadeDuration: TimeInterval = 0.2
|
||
|
||
/**
|
||
Activity indicator color. Default is `.white`.
|
||
*/
|
||
public var activityIndicatorColor: UIColor = .white
|
||
|
||
/**
|
||
Activity background color. Default is `.black` at 80% opacity.
|
||
*/
|
||
public var activityBackgroundColor: UIColor = UIColor.black.withAlphaComponent(0.8)
|
||
}
|
||
|
||
// MARK: - Toast Manager
|
||
|
||
/**
|
||
`ToastManager` provides general configuration options for all toast
|
||
notifications. Backed by a singleton instance.
|
||
*/
|
||
public class ToastManager {
|
||
/**
|
||
The `ToastManager` singleton instance.
|
||
|
||
*/
|
||
public static let shared = ToastManager()
|
||
|
||
/**
|
||
The shared style. Used whenever toastViewForMessage(message:title:image:style:) is called
|
||
with with a nil style.
|
||
|
||
*/
|
||
public var style = ToastStyle()
|
||
|
||
/**
|
||
Enables or disables tap to dismiss on toast views. Default is `true`.
|
||
|
||
*/
|
||
public var isTapToDismissEnabled = true
|
||
|
||
/**
|
||
Enables or disables queueing behavior for toast views. When `true`,
|
||
toast views will appear one after the other. When `false`, multiple toast
|
||
views will appear at the same time (potentially overlapping depending
|
||
on their positions). This has no effect on the toast activity view,
|
||
which operates independently of normal toast views. Default is `false`.
|
||
|
||
*/
|
||
public var isQueueEnabled = false
|
||
|
||
/**
|
||
The default duration. Used for the `makeToast` and
|
||
`showToast` methods that don't require an explicit duration.
|
||
Default is 3.0.
|
||
|
||
*/
|
||
public var duration: TimeInterval = 1.0
|
||
|
||
/**
|
||
Sets the default position. Used for the `makeToast` and
|
||
`showToast` methods that don't require an explicit position.
|
||
Default is `ToastPosition.Bottom`.
|
||
|
||
*/
|
||
public var position: ToastPosition = .center
|
||
}
|
||
|
||
// MARK: - ToastPosition
|
||
|
||
public enum ToastPosition {
|
||
case top
|
||
case center
|
||
case bottom
|
||
|
||
fileprivate func centerPoint(forToast toast: UIView, inSuperview superview: UIView) -> CGPoint {
|
||
let topPadding: CGFloat = ToastManager.shared.style.verticalPadding + superview.csSafeAreaInsets.top
|
||
let bottomPadding: CGFloat = ToastManager.shared.style.verticalPadding + superview.csSafeAreaInsets.bottom
|
||
|
||
switch self {
|
||
case .top:
|
||
return CGPoint(x: superview.bounds.size.width / 2.0, y: (toast.frame.size.height / 2.0) + topPadding)
|
||
case .center:
|
||
return CGPoint(x: superview.bounds.size.width / 2.0, y: superview.bounds.size.height / 2.0)
|
||
case .bottom:
|
||
return CGPoint(x: superview.bounds.size.width / 2.0, y: (superview.bounds.size.height - (toast.frame.size.height / 2.0)) - bottomPadding)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Private UIView Extensions
|
||
|
||
private extension UIView {
|
||
var csSafeAreaInsets: UIEdgeInsets {
|
||
if #available(iOS 11.0, *) {
|
||
return self.safeAreaInsets
|
||
} else {
|
||
return .zero
|
||
}
|
||
}
|
||
}
|