Issue #472
Build error view
Use convenient code from Omnia
To make view height dynamic, pin UILabel
to edges and center
import UIKit
final 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
import UIKit
final 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
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.
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 {
// No op
}
})
}