ToastView.swift 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import UIKit
  2. open class ToastView: UIView {
  3. // MARK: Properties
  4. open var text: String? {
  5. get { return self.textLabel.text }
  6. set { self.textLabel.text = newValue }
  7. }
  8. open var attributedText: NSAttributedString? {
  9. get { return self.textLabel.attributedText }
  10. set { self.textLabel.attributedText = newValue }
  11. }
  12. // MARK: Appearance
  13. /// The background view's color.
  14. override open dynamic var backgroundColor: UIColor? {
  15. get { return self.backgroundView.backgroundColor }
  16. set { self.backgroundView.backgroundColor = newValue }
  17. }
  18. /// The background view's corner radius.
  19. @objc open dynamic var cornerRadius: CGFloat {
  20. get { return self.backgroundView.layer.cornerRadius }
  21. set { self.backgroundView.layer.cornerRadius = newValue }
  22. }
  23. /// The inset of the text label.
  24. @objc open dynamic var textInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
  25. /// The color of the text label's text.
  26. @objc open dynamic var textColor: UIColor? {
  27. get { return self.textLabel.textColor }
  28. set { self.textLabel.textColor = newValue }
  29. }
  30. /// The font of the text label.
  31. @objc open dynamic var font: UIFont? {
  32. get { return self.textLabel.font }
  33. set { self.textLabel.font = newValue }
  34. }
  35. /// The bottom offset from the screen's bottom in portrait mode.
  36. @objc open dynamic var bottomOffsetPortrait: CGFloat = {
  37. switch UIDevice.current.userInterfaceIdiom {
  38. // specific values
  39. case .phone: return 30
  40. case .pad: return 60
  41. case .tv: return 90
  42. case .carPlay: return 30
  43. #if compiler(>=5.3)
  44. case .mac: return 60
  45. #endif
  46. // default values
  47. case .unspecified: fallthrough
  48. @unknown default: return 30
  49. }
  50. }()
  51. /// The bottom offset from the screen's bottom in landscape mode.
  52. @objc open dynamic var bottomOffsetLandscape: CGFloat = {
  53. switch UIDevice.current.userInterfaceIdiom {
  54. // specific values
  55. case .phone: return 20
  56. case .pad: return 40
  57. case .tv: return 60
  58. case .carPlay: return 20
  59. #if compiler(>=5.3)
  60. case .mac: return 40
  61. #endif
  62. // default values
  63. case .unspecified: fallthrough
  64. @unknown default: return 20
  65. }
  66. }()
  67. /// If this value is `true` and SafeArea is available,
  68. /// `safeAreaInsets.bottom` will be added to the `bottomOffsetPortrait` and `bottomOffsetLandscape`.
  69. /// Default value: false
  70. @objc open dynamic var useSafeAreaForBottomOffset: Bool = false
  71. /// The width ratio of toast view in window, specified as a value from 0.0 to 1.0.
  72. /// Default value: 0.875
  73. @objc open dynamic var maxWidthRatio: CGFloat = (280.0 / 320.0)
  74. /// The shape of the layer’s shadow.
  75. @objc open dynamic var shadowPath: CGPath? {
  76. get { return self.layer.shadowPath }
  77. set { self.layer.shadowPath = newValue }
  78. }
  79. /// The color of the layer’s shadow.
  80. @objc open dynamic var shadowColor: UIColor? {
  81. get { return self.layer.shadowColor.flatMap { UIColor(cgColor: $0) } }
  82. set { self.layer.shadowColor = newValue?.cgColor }
  83. }
  84. /// The opacity of the layer’s shadow.
  85. @objc open dynamic var shadowOpacity: Float {
  86. get { return self.layer.shadowOpacity }
  87. set { self.layer.shadowOpacity = newValue }
  88. }
  89. /// The offset (in points) of the layer’s shadow.
  90. @objc open dynamic var shadowOffset: CGSize {
  91. get { return self.layer.shadowOffset }
  92. set { self.layer.shadowOffset = newValue }
  93. }
  94. /// The blur radius (in points) used to render the layer’s shadow.
  95. @objc open dynamic var shadowRadius: CGFloat {
  96. get { return self.layer.shadowRadius }
  97. set { self.layer.shadowRadius = newValue }
  98. }
  99. // MARK: UI
  100. private let backgroundView: UIView = {
  101. let `self` = UIView()
  102. self.backgroundColor = UIColor(white: 0, alpha: 0.7)
  103. self.layer.cornerRadius = 5
  104. self.clipsToBounds = true
  105. return self
  106. }()
  107. private let textLabel: UILabel = {
  108. let `self` = UILabel()
  109. self.textColor = .white
  110. self.backgroundColor = .clear
  111. self.font = {
  112. switch UIDevice.current.userInterfaceIdiom {
  113. // specific values
  114. case .phone: return .systemFont(ofSize: 12)
  115. case .pad: return .systemFont(ofSize: 16)
  116. case .tv: return .systemFont(ofSize: 20)
  117. case .carPlay: return .systemFont(ofSize: 12)
  118. #if compiler(>=5.3)
  119. case .mac: return .systemFont(ofSize: 16)
  120. #endif
  121. // default values
  122. case .unspecified: fallthrough
  123. @unknown default: return .systemFont(ofSize: 12)
  124. }
  125. }()
  126. self.numberOfLines = 0
  127. self.textAlignment = .center
  128. return self
  129. }()
  130. // MARK: Initializing
  131. public init() {
  132. super.init(frame: .zero)
  133. self.isUserInteractionEnabled = false
  134. self.addSubview(self.backgroundView)
  135. self.addSubview(self.textLabel)
  136. }
  137. required convenience public init?(coder aDecoder: NSCoder) {
  138. self.init()
  139. }
  140. // MARK: Layout
  141. override open func layoutSubviews() {
  142. super.layoutSubviews()
  143. let containerSize = ToastWindow.shared.frame.size
  144. let constraintSize = CGSize(
  145. width: containerSize.width * maxWidthRatio - self.textInsets.left - self.textInsets.right,
  146. height: CGFloat.greatestFiniteMagnitude
  147. )
  148. let textLabelSize = self.textLabel.sizeThatFits(constraintSize)
  149. self.textLabel.frame = CGRect(
  150. x: self.textInsets.left,
  151. y: self.textInsets.top,
  152. width: textLabelSize.width,
  153. height: textLabelSize.height
  154. )
  155. self.backgroundView.frame = CGRect(
  156. x: 0,
  157. y: 0,
  158. width: self.textLabel.frame.size.width + self.textInsets.left + self.textInsets.right,
  159. height: self.textLabel.frame.size.height + self.textInsets.top + self.textInsets.bottom
  160. )
  161. var x: CGFloat
  162. var y: CGFloat
  163. var width: CGFloat
  164. var height: CGFloat
  165. let orientation = UIApplication.shared.statusBarOrientation
  166. if orientation.isPortrait || !ToastWindow.shared.shouldRotateManually {
  167. width = containerSize.width
  168. height = containerSize.height
  169. y = self.bottomOffsetPortrait
  170. } else {
  171. width = containerSize.height
  172. height = containerSize.width
  173. y = self.bottomOffsetLandscape
  174. }
  175. if #available(iOS 11.0, *), useSafeAreaForBottomOffset {
  176. y += ToastWindow.shared.safeAreaInsets.bottom
  177. }
  178. let backgroundViewSize = self.backgroundView.frame.size
  179. x = (width - backgroundViewSize.width) * 0.5
  180. y = height - (backgroundViewSize.height + y)
  181. self.frame = CGRect(
  182. x: x,
  183. y: y,
  184. width: backgroundViewSize.width,
  185. height: backgroundViewSize.height
  186. )
  187. }
  188. override open func hitTest(_ point: CGPoint, with event: UIEvent!) -> UIView? {
  189. if let superview = self.superview {
  190. let pointInWindow = self.convert(point, to: superview)
  191. let contains = self.frame.contains(pointInWindow)
  192. if contains && self.isUserInteractionEnabled {
  193. return self
  194. }
  195. }
  196. return nil
  197. }
  198. }