Files
2024-01-23 12:15:13 -06:00

449 lines
16 KiB
Swift

//
// MessageView.swift
// SwiftMessages
//
// Created by Timothy Moose on 7/30/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
/*
*/
open class MessageView: BaseView, Identifiable, AccessibleMessage, HapticMessage {
/*
MARK: - Haptic feedback
*/
/// The default haptic feedback to be used when the message is presented.
open var defaultHaptic: SwiftMessages.Haptic?
/*
MARK: - Button tap handler
*/
/// An optional button tap handler. The `button` is automatically
/// configured to call this tap handler on `.TouchUpInside`.
open var buttonTapHandler: ((_ button: UIButton) -> Void)?
@objc func buttonTapped(_ button: UIButton) {
buttonTapHandler?(button)
}
/*
MARK: - Touch handling
*/
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Only accept touches within the background view. Anything outside of the
// background view's bounds should be transparent and does not need to receive
// touches. This helps with tap dismissal when using `DimMode.gray` and `DimMode.color`.
return backgroundView == self
? super.point(inside: point, with: event)
: backgroundView.point(inside: convert(point, to: backgroundView), with: event)
}
/*
MARK: - IB outlets
*/
/// An optional title label.
@IBOutlet open var titleLabel: UILabel?
/// An optional body text label.
@IBOutlet open var bodyLabel: UILabel?
/// An optional icon image view.
@IBOutlet open var iconImageView: UIImageView?
/// An optional icon label (e.g. for emoji character, icon font, etc.).
@IBOutlet open var iconLabel: UILabel?
/// An optional button. This buttons' `.TouchUpInside` event will automatically
/// invoke the optional `buttonTapHandler`, but its fine to add other target
/// action handlers can be added.
@IBOutlet open var button: UIButton? {
didSet {
if let old = oldValue {
old.removeTarget(self, action: #selector(MessageView.buttonTapped(_:)), for: .touchUpInside)
}
if let button = button {
button.addTarget(self, action: #selector(MessageView.buttonTapped(_:)), for: .touchUpInside)
}
}
}
/*
MARK: - Identifiable
*/
open var id: String {
get {
return customId ?? "MessageView:title=\(String(describing: titleLabel?.text)), body=\(String(describing: bodyLabel?.text))"
}
set {
customId = newValue
}
}
private var customId: String?
/*
MARK: - AccessibleMessage
*/
/**
An optional prefix for the `accessibilityMessage` that can
be used to further clarify the message for VoiceOver. For example,
the view's background color or icon might convey that a message is
a warning, in which case one may specify the value "warning".
*/
open var accessibilityPrefix: String?
open var accessibilityMessage: String? {
#if swift(>=4.1)
let components = [accessibilityPrefix, titleLabel?.text, bodyLabel?.text].compactMap { $0 }
#else
let components = [accessibilityPrefix, titleLabel?.text, bodyLabel?.text].flatMap { $0 }
#endif
guard components.count > 0 else { return nil }
return components.joined(separator: ", ")
}
public var accessibilityElement: NSObject? {
return backgroundView
}
open var additionalAccessibilityElements: [NSObject]? {
var elements: [NSObject] = []
func getAccessibleSubviews(view: UIView) {
for subview in view.subviews {
if subview.isAccessibilityElement {
elements.append(subview)
} else {
// Only doing this for non-accessible `subviews`, which avoids
// including button labels, etc.
getAccessibleSubviews(view: subview)
}
}
}
getAccessibleSubviews(view: self.backgroundView)
return elements
}
}
/*
MARK: - Creating message views
This extension provides several convenience functions for instantiating
`MessageView` from the included nib files in a type-safe way. These nib
files can be found in the Resources folder and can be drag-and-dropped
into a project and modified. You may still use these APIs if you've
copied the nib files because SwiftMessages looks for them in the main
bundle first. See `SwiftMessages` for additional nib loading options.
*/
extension MessageView {
/**
Specifies one of the nib files included in the Resources folders.
*/
public enum Layout: String {
/**
The standard message view that stretches across the full width of the
container view.
*/
case messageView = "MessageView"
/**
A floating card-style view with rounded corners.
*/
case cardView = "CardView"
/**
Like `CardView` with one end attached to the super view.
*/
case tabView = "TabView"
/**
A 20pt tall view that can be used to overlay the status bar.
Note that this layout will automatically grow taller if displayed
directly under the status bar (see the `ContentInsetting` protocol).
*/
case statusLine = "StatusLine"
/**
A floating card-style view with elements centered and arranged vertically.
This view is typically used with `.center` presentation style.
*/
case centeredView = "CenteredView"
}
/**
Loads the nib file associated with the given `Layout` and returns the first
view found in the nib file with the matching type `T: MessageView`.
- Parameter layout: The `Layout` option to use.
- Parameter filesOwner: An optional files owner.
- Returns: An instance of generic view type `T: MessageView`.
*/
public static func viewFromNib<T: MessageView>(layout: Layout, filesOwner: AnyObject = NSNull.init()) -> T {
return try! SwiftMessages.viewFromNib(named: layout.rawValue)
}
/**
Loads the nib file associated with the given `Layout` from
the given bundle and returns the first view found in the nib
file with the matching type `T: MessageView`.
- Parameter layout: The `Layout` option to use.
- Parameter bundle: The name of the bundle containing the nib file.
- Parameter filesOwner: An optional files owner.
- Returns: An instance of generic view type `T: MessageView`.
*/
public static func viewFromNib<T: MessageView>(layout: Layout, bundle: Bundle, filesOwner: AnyObject = NSNull.init()) -> T {
return try! SwiftMessages.viewFromNib(named: layout.rawValue, bundle: bundle, filesOwner: filesOwner)
}
}
/*
MARK: - Layout adjustments
This extension provides a few convenience functions for adjusting the layout.
*/
extension MessageView {
/**
Constrains the image view to a specified size. By default, the size of the
image view is determined by its `intrinsicContentSize`.
- Parameter size: The size to be translated into Auto Layout constraints.
- Parameter contentMode: The optional content mode to apply.
*/
public func configureIcon(withSize size: CGSize, contentMode: UIView.ContentMode? = nil) {
var views: [UIView] = []
if let iconImageView = iconImageView { views.append(iconImageView) }
if let iconLabel = iconLabel { views.append(iconLabel) }
views.forEach {
let constraints = [$0.heightAnchor.constraint(equalToConstant: size.height),
$0.widthAnchor.constraint(equalToConstant: size.width)]
constraints.forEach { $0.priority = UILayoutPriority(999.0) }
$0.addConstraints(constraints)
if let contentMode = contentMode {
$0.contentMode = contentMode
}
}
}
}
/*
MARK: - Theming
This extension provides a few convenience functions for setting styles,
colors and icons. You are encouraged to write your own such functions
if these don't exactly meet your needs.
*/
extension MessageView {
/**
A convenience function for setting some pre-defined colors and icons.
- Parameter theme: The theme type to use.
- Parameter iconStyle: The icon style to use. Defaults to `.Default`.
- Parameter useHaptics: If `true`, configures an appropriate haptic based on theme. Defaults to `false`.
*/
public func configureTheme(_ theme: Theme, iconStyle: IconStyle = .default, includeHaptic: Bool = false) {
let iconImage = iconStyle.image(theme: theme)
let backgroundColor: UIColor
let foregroundColor: UIColor
let defaultBackgroundColor: UIColor
switch theme {
case .info:
defaultBackgroundColor = UIColor(red: 225.0/255.0, green: 225.0/255.0, blue: 225.0/255.0, alpha: 1.0)
case .success:
defaultBackgroundColor = UIColor(red: 97.0/255.0, green: 161.0/255.0, blue: 23.0/255.0, alpha: 1.0)
case .warning:
defaultBackgroundColor = UIColor(red: 246.0/255.0, green: 197.0/255.0, blue: 44.0/255.0, alpha: 1.0)
case .error:
defaultBackgroundColor = UIColor(red: 249.0/255.0, green: 66.0/255.0, blue: 47.0/255.0, alpha: 1.0)
}
if includeHaptic {
switch theme {
case .success, .info:
defaultHaptic = SwiftMessages.Haptic.success
case .warning:
defaultHaptic = SwiftMessages.Haptic.warning
case .error:
defaultHaptic = SwiftMessages.Haptic.error
}
}
switch theme {
case .info:
backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark, .unspecified: return UIColor(red: 125/255.0, green: 125/255.0, blue: 125/255.0, alpha: 1.0)
case .light: fallthrough
@unknown default:
return defaultBackgroundColor
}
}
foregroundColor = .label
case .success:
backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark, .unspecified: return UIColor(red: 55/255.0, green: 122/255.0, blue: 0/255.0, alpha: 1.0)
case .light: fallthrough
@unknown default:
return defaultBackgroundColor
}
}
foregroundColor = .white
case .warning:
backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark, .unspecified: return UIColor(red: 239/255.0, green: 184/255.0, blue: 10/255.0, alpha: 1.0)
case .light: fallthrough
@unknown default:
return defaultBackgroundColor
}
}
foregroundColor = .white
case .error:
backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark, .unspecified: return UIColor(red: 195/255.0, green: 12/255.0, blue: 12/255.0, alpha: 1.0)
case .light: fallthrough
@unknown default:
return defaultBackgroundColor
}
}
foregroundColor = .white
}
configureTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconImage: iconImage)
}
/**
A convenience function for setting a foreground and background color.
Note that images will only display the foreground color if they're
configured with UIImageRenderingMode.AlwaysTemplate.
- Parameter backgroundColor: The background color to use.
- Parameter foregroundColor: The foreground color to use.
*/
public func configureTheme(backgroundColor: UIColor, foregroundColor: UIColor, iconImage: UIImage? = nil, iconText: String? = nil) {
iconImageView?.image = iconImage
iconLabel?.text = iconText
iconImageView?.tintColor = foregroundColor
let backgroundView = self.backgroundView ?? self
backgroundView.backgroundColor = backgroundColor
iconLabel?.textColor = foregroundColor
titleLabel?.textColor = foregroundColor
bodyLabel?.textColor = foregroundColor
button?.backgroundColor = foregroundColor
button?.tintColor = backgroundColor
button?.contentEdgeInsets = UIEdgeInsets(top: 7.0, left: 7.0, bottom: 7.0, right: 7.0)
button?.layer.cornerRadius = 5.0
iconImageView?.isHidden = iconImageView?.image == nil
iconLabel?.isHidden = iconLabel?.text == nil
}
}
/*
MARK: - Configuring the content
This extension provides a few convenience functions for configuring the
message content. You are encouraged to write your own such functions
if these don't exactly meet your needs.
SwiftMessages does not try to be clever by adjusting the layout based on
what content you configure. All message elements are optional and it is
up to you to hide or remove elements you don't need. The easiest way to
remove unwanted elements is to drag-and-drop one of the included nib
files into your project as a starting point and make changes.
*/
extension MessageView {
/**
Sets the message body text.
- Parameter body: The message body text to use.
*/
public func configureContent(body: String) {
bodyLabel?.text = body
}
/**
Sets the message title and body text.
- Parameter title: The message title to use.
- Parameter body: The message body text to use.
*/
public func configureContent(title: String, body: String) {
configureContent(body: body)
titleLabel?.text = title
}
/**
Sets the message title, body text and icon image. Also hides the
`iconLabel`.
- Parameter title: The message title to use.
- Parameter body: The message body text to use.
- Parameter iconImage: The icon image to use.
*/
public func configureContent(title: String, body: String, iconImage: UIImage) {
configureContent(title: title, body: body)
iconImageView?.image = iconImage
iconImageView?.isHidden = false
iconLabel?.text = nil
iconLabel?.isHidden = true
}
/**
Sets the message title, body text and icon text (e.g. an emoji).
Also hides the `iconImageView`.
- Parameter title: The message title to use.
- Parameter body: The message body text to use.
- Parameter iconText: The icon text to use (e.g. an emoji).
*/
public func configureContent(title: String, body: String, iconText: String) {
configureContent(title: title, body: body)
iconLabel?.text = iconText
iconLabel?.isHidden = false
iconImageView?.isHidden = true
iconImageView?.image = nil
}
/**
Sets all configurable elements.
- Parameter title: The message title to use.
- Parameter body: The message body text to use.
- Parameter iconImage: The icon image to use.
- Parameter iconText: The icon text to use (e.g. an emoji).
- Parameter buttonImage: The button image to use.
- Parameter buttonTitle: The button title to use.
- Parameter buttonTapHandler: The button tap handler block to use.
*/
public func configureContent(title: String?, body: String?, iconImage: UIImage?, iconText: String?, buttonImage: UIImage?, buttonTitle: String?, buttonTapHandler: ((_ button: UIButton) -> Void)?) {
titleLabel?.text = title
bodyLabel?.text = body
iconImageView?.image = iconImage
iconLabel?.text = iconText
button?.setImage(buttonImage, for: .normal)
button?.setTitle(buttonTitle, for: .normal)
self.buttonTapHandler = buttonTapHandler
iconImageView?.isHidden = iconImageView?.image == nil
iconLabel?.isHidden = iconLabel?.text == nil
}
}