MessageViewController 2.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import UIKit
  2. /// View controller used for showing info text and loading animation.
  3. public final class MessageViewController: UIViewController {
  4. // Image tint color for all the states, except for `.notFound`.
  5. public var regularTintColor: UIColor = .black
  6. // Image tint color for `.notFound` state.
  7. public var errorTintColor: UIColor = .red
  8. // Customizable state messages.
  9. public var messages = StateMessageProvider()
  10. // MARK: - UI properties
  11. /// Text label.
  12. public private(set) lazy var textLabel: UILabel = self.makeTextLabel()
  13. /// Info image view.
  14. public private(set) lazy var imageView: UIImageView = self.makeImageView()
  15. /// Border view.
  16. public private(set) lazy var borderView: UIView = self.makeBorderView()
  17. /// Blur effect view.
  18. private lazy var blurView: UIVisualEffectView = .init(effect: UIBlurEffect(style: .extraLight))
  19. // Constraints that are activated when the view is used as a footer.
  20. private lazy var collapsedConstraints: [NSLayoutConstraint] = self.makeCollapsedConstraints()
  21. // Constraints that are activated when the view is used for loading animation and error messages.
  22. private lazy var expandedConstraints: [NSLayoutConstraint] = self.makeExpandedConstraints()
  23. var status = Status(state: .scanning) {
  24. didSet {
  25. handleStatusUpdate()
  26. }
  27. }
  28. // MARK: - View lifecycle
  29. public override func viewDidLoad() {
  30. super.viewDidLoad()
  31. view.addSubview(blurView)
  32. blurView.contentView.addSubviews(textLabel, imageView, borderView)
  33. handleStatusUpdate()
  34. }
  35. public override func viewDidLayoutSubviews() {
  36. super.viewDidLayoutSubviews()
  37. blurView.frame = view.bounds
  38. }
  39. // MARK: - Animations
  40. /// Animates blur and border view.
  41. func animateLoading() {
  42. animate(blurStyle: .light)
  43. animate(borderViewAngle: CGFloat(Double.pi/2))
  44. }
  45. /**
  46. Animates blur to make pulsating effect.
  47. - Parameter style: The current blur style.
  48. */
  49. private func animate(blurStyle: UIBlurEffectStyle) {
  50. guard status.state == .processing else { return }
  51. UIView.animate(
  52. withDuration: 2.0,
  53. delay: 0.5,
  54. options: [.beginFromCurrentState],
  55. animations: ({ [weak self] in
  56. self?.blurView.effect = UIBlurEffect(style: blurStyle)
  57. }),
  58. completion: ({ [weak self] _ in
  59. self?.animate(blurStyle: blurStyle == .light ? .extraLight : .light)
  60. }))
  61. }
  62. /**
  63. Animates border view with a given angle.
  64. - Parameter angle: Rotation angle.
  65. */
  66. private func animate(borderViewAngle: CGFloat) {
  67. guard status.state == .processing else {
  68. borderView.transform = .identity
  69. return
  70. }
  71. UIView.animate(
  72. withDuration: 0.8,
  73. delay: 0.5,
  74. usingSpringWithDamping: 0.6,
  75. initialSpringVelocity: 1.0,
  76. options: [.beginFromCurrentState],
  77. animations: ({ [weak self] in
  78. self?.borderView.transform = CGAffineTransform(rotationAngle: borderViewAngle)
  79. }),
  80. completion: ({ [weak self] _ in
  81. self?.animate(borderViewAngle: borderViewAngle + CGFloat(Double.pi / 2))
  82. }))
  83. }
  84. // MARK: - State handling
  85. private func handleStatusUpdate() {
  86. borderView.isHidden = true
  87. borderView.layer.removeAllAnimations()
  88. textLabel.text = status.text ?? messages.makeText(for: status.state)
  89. switch status.state {
  90. case .scanning, .unauthorized:
  91. textLabel.numberOfLines = 3
  92. textLabel.textAlignment = .left
  93. imageView.tintColor = regularTintColor
  94. case .processing:
  95. textLabel.numberOfLines = 10
  96. textLabel.textAlignment = .center
  97. borderView.isHidden = false
  98. imageView.tintColor = regularTintColor
  99. case .notFound:
  100. textLabel.font = UIFont.boldSystemFont(ofSize: 16)
  101. textLabel.numberOfLines = 10
  102. textLabel.textAlignment = .center
  103. imageView.tintColor = errorTintColor
  104. }
  105. if status.state == .scanning || status.state == .unauthorized {
  106. expandedConstraints.deactivate()
  107. collapsedConstraints.activate()
  108. } else {
  109. collapsedConstraints.deactivate()
  110. expandedConstraints.activate()
  111. }
  112. }
  113. }
  114. // MARK: - Subviews factory
  115. private extension MessageViewController {
  116. func makeTextLabel() -> UILabel {
  117. let label = UILabel()
  118. label.translatesAutoresizingMaskIntoConstraints = false
  119. label.textColor = .black
  120. label.numberOfLines = 3
  121. label.font = UIFont.boldSystemFont(ofSize: 14)
  122. return label
  123. }
  124. func makeImageView() -> UIImageView {
  125. let imageView = UIImageView()
  126. imageView.translatesAutoresizingMaskIntoConstraints = false
  127. imageView.image = imageNamed("info").withRenderingMode(.alwaysTemplate)
  128. imageView.tintColor = .black
  129. return imageView
  130. }
  131. func makeBorderView() -> UIView {
  132. let view = UIView()
  133. view.translatesAutoresizingMaskIntoConstraints = false
  134. view.backgroundColor = .clear
  135. view.layer.borderWidth = 2
  136. view.layer.cornerRadius = 10
  137. view.layer.borderColor = UIColor.black.cgColor
  138. return view
  139. }
  140. }
  141. // MARK: - Layout
  142. extension MessageViewController {
  143. private func makeExpandedConstraints() -> [NSLayoutConstraint] {
  144. let padding: CGFloat = 10
  145. let borderSize: CGFloat = 51
  146. return [
  147. imageView.centerYAnchor.constraint(equalTo: blurView.centerYAnchor, constant: -60),
  148. imageView.centerXAnchor.constraint(equalTo: blurView.centerXAnchor),
  149. imageView.widthAnchor.constraint(equalToConstant: 30),
  150. imageView.heightAnchor.constraint(equalToConstant: 27),
  151. textLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 18),
  152. textLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
  153. textLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding),
  154. borderView.topAnchor.constraint(equalTo: imageView.topAnchor, constant: -12),
  155. borderView.centerXAnchor.constraint(equalTo: blurView.centerXAnchor),
  156. borderView.widthAnchor.constraint(equalToConstant: borderSize),
  157. borderView.heightAnchor.constraint(equalToConstant: borderSize)
  158. ]
  159. }
  160. private func makeCollapsedConstraints() -> [NSLayoutConstraint] {
  161. let padding: CGFloat = 10
  162. var constraints = [
  163. imageView.topAnchor.constraint(equalTo: blurView.topAnchor, constant: 18),
  164. imageView.widthAnchor.constraint(equalToConstant: 30),
  165. imageView.heightAnchor.constraint(equalToConstant: 27),
  166. textLabel.topAnchor.constraint(equalTo: imageView.topAnchor, constant: -3),
  167. textLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10)
  168. ]
  169. if #available(iOS 11.0, *) {
  170. constraints += [
  171. imageView.leadingAnchor.constraint(
  172. equalTo: view.safeAreaLayoutGuide.leadingAnchor,
  173. constant: padding
  174. ),
  175. textLabel.trailingAnchor.constraint(
  176. equalTo: view.safeAreaLayoutGuide.trailingAnchor,
  177. constant: -padding
  178. )
  179. ]
  180. } else {
  181. constraints += [
  182. imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
  183. textLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding)
  184. ]
  185. }
  186. return constraints
  187. }
  188. }