Issue #472
Build error view Use convenient code from Omnia
To make view height dynamic, pin UILabel
to edges and center
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import UIKitfinal class ErrorMessageView : UIView { let box: UIView = { let view = UIView () view.backgroundColor = R .color.primary view.layer.cornerRadius = 6 return view }() let label: UILabel = { let label = UILabel () label.styleAsText() label.textColor = R .color.darkText label.numberOfLines = 0 return label }() override init (frame: CGRect ) { super .init (frame: frame) setup() } required init ?(coder: NSCoder ) { fatalError () } private func setup () { addSubviews([box, label]) NSLayoutConstraint .on([ box.pinEdges(view: self , inset: UIEdgeInsets .all(16 )), label.pinEdges(view: box, inset: UIEdgeInsets .all(8 )) ]) NSLayoutConstraint .on([ box.heightAnchor.constraint(greaterThanOrEqualToConstant: 48 ) ]) NSLayoutConstraint .on([ label.centerYAnchor.constraint(equalTo: centerYAnchor) ]) } }
Show and hide Use Auto Layout and basic UIView animation. Use debouncer to avoid hide
gets called for the new show
. Use debouncer instead of DispatchQueue.main.asyncAfter
because it can cancel the previous DispatchWorkItem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import UIKitfinal class ErrorMessageHandler { let view: UIView let errorMessageView = ErrorMessageView () let debouncer = Debouncer (delay: 0.5 ) init (view: UIView ) { self .view = view } func show (text: String) { self .errorMessageView.label.text = text view.addSubview(errorMessageView) NSLayoutConstraint .on([ errorMessageView.leftAnchor.constraint(equalTo: view.leftAnchor), errorMessageView.rightAnchor.constraint(equalTo: view.rightAnchor), errorMessageView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) ]) toggle(shows: true ) debouncer.run { self .hide() } } func hide () { toggle(shows: false ) } private func toggle (shows: Bool) { self .errorMessageView.alpha = shows ? 0 : 1.0 UIView .animate(withDuration: 0.25 , animations: { self .errorMessageView.alpha = shows ? 1.0 : 0 }, completion: { _ in if shows { self .view.bringSubviewToFront(self .errorMessageView) } else { self .errorMessageView.removeFromSuperview() } }) } }
Handle keyboard If we add this error message on UIView in ViewController and we use KeyboardHandler to scroll the entire view, then this snack bar will move up as well
1 2 3 4 5 6 7 8 9 10 final class ErrorMessageHandler { private let errorMessageView = ErrorMessageView () private var view = UIView () private var bottomOffset: CGFloat = 0 func on (view: UIView, bottomOffset: CGFloat) { self .view = view self .bottomOffset = bottomOffset } }
UIView animation completion One tricky thing is that if we call hide
and then show
immediately, the completion of hide
will be called after and then remove the view.
When we start animation again, the previous animation is not finished, so we need to check
Read UIView.animate
completion A block object to be executed when the animation sequence ends. This block has no return value and takes a single Boolean argument that indicates whether or not the animations actually finished before the completion handler was called. If the duration of the animation is 0, this block is performed at the beginning of the next run loop cycle. This parameter may be NULL.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private func toggle (shows: Bool) { self .errorMessageView.alpha = shows ? 0 : 1.0 UIView .animate(withDuration: 0.25 , delay: 0 , options: .curveEaseInOut, animations: { self .errorMessageView.alpha = shows ? 1.0 : 0 }, completion: { finished in if shows { self .view.bringSubviewToFront(self .errorMessageView) } else if finished { self .errorMessageView.removeFromSuperview() } else { } }) }