Dactyloidae/mobile/ios/Client/Frontend/Browser/ButtonToast.swift
2026-06-26 21:04:09 -07:00

181 lines
7 KiB
Swift

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Shared
import SnapKit
struct ButtonToastUX {
static let ToastPadding = 15.0
static let TitleSpacing = 2.0
static let ToastButtonPadding: CGFloat = 10.0
static let TitleButtonPadding: CGFloat = 5.0
static let ToastDelay = DispatchTimeInterval.milliseconds(900)
static let ToastButtonBorderRadius: CGFloat = 5
static let ToastButtonBorderWidth: CGFloat = 1
}
private class HighlightableButton: UIButton {
override var isHighlighted: Bool {
didSet {
if isHighlighted {
self.backgroundColor = UIColor.white
} else {
self.backgroundColor = UIColor.clear
}
}
}
}
class ButtonToast: UIView {
fileprivate var dismissed = false
fileprivate var completionHandler: ((Bool) -> Void)?
fileprivate lazy var toast: UIView = {
let toast = UIView()
toast.backgroundColor = SimpleToastUX.ToastDefaultColor
return toast
}()
fileprivate var animationConstraint: Constraint?
fileprivate lazy var gestureRecognizer: UITapGestureRecognizer = {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ButtonToast.handleTap(_:)))
gestureRecognizer.cancelsTouchesInView = false
return gestureRecognizer
}()
init(labelText: String, descriptionText: String? = nil, buttonText: String, completion:@escaping (_ buttonPressed: Bool) -> Void) {
super.init(frame: CGRect.zero)
completionHandler = completion
self.clipsToBounds = true
self.addSubview(createView(labelText, descriptionText: descriptionText, buttonText: buttonText))
toast.snp.makeConstraints { make in
make.left.right.height.equalTo(self)
animationConstraint = make.top.equalTo(self).offset(SimpleToastUX.ToastHeight).constraint
}
self.snp.makeConstraints { make in
make.height.equalTo(SimpleToastUX.ToastHeight)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func createView(_ labelText: String, descriptionText: String?, buttonText: String) -> UIView {
let label = UILabel()
label.textColor = UIColor.white
label.font = SimpleToastUX.ToastFont
label.text = labelText
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
toast.addSubview(label)
let button = HighlightableButton()
button.layer.cornerRadius = ButtonToastUX.ToastButtonBorderRadius
button.layer.borderWidth = ButtonToastUX.ToastButtonBorderWidth
button.layer.borderColor = UIColor.white.cgColor
button.setTitle(buttonText, for: UIControlState())
button.setTitleColor(self.toast.backgroundColor, for: .highlighted)
button.titleLabel?.font = SimpleToastUX.ToastFont
button.titleLabel?.numberOfLines = 1
button.titleLabel?.lineBreakMode = .byClipping
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.minimumScaleFactor = 0.1
let recognizer = UITapGestureRecognizer(target: self, action: #selector(ButtonToast.buttonPressed(_:)))
button.addGestureRecognizer(recognizer)
toast.addSubview(button)
var descriptionLabel: UILabel?
if let text = descriptionText {
let textLabel = UILabel()
textLabel.textColor = UIColor.white
textLabel.font = SimpleToastUX.ToastFont
textLabel.text = text
textLabel.lineBreakMode = .byTruncatingTail
toast.addSubview(textLabel)
descriptionLabel = textLabel
}
if let description = descriptionLabel {
label.numberOfLines = 1 // if showing a description we cant wrap to the second line
label.lineBreakMode = .byClipping
label.adjustsFontSizeToFitWidth = true
label.snp.makeConstraints { (make) in
make.leading.equalTo(toast).offset(ButtonToastUX.ToastPadding)
make.top.equalTo(toast).offset(ButtonToastUX.TitleButtonPadding)
make.trailing.equalTo(button.snp.leading).offset(-ButtonToastUX.TitleButtonPadding)
}
description.snp.makeConstraints { (make) in
make.leading.equalTo(toast).offset(ButtonToastUX.ToastPadding)
make.top.equalTo(label.snp.bottom).offset(ButtonToastUX.TitleSpacing)
make.trailing.equalTo(button.snp.leading).offset(-ButtonToastUX.TitleButtonPadding)
}
} else {
label.snp.makeConstraints { (make) in
make.leading.equalTo(toast).offset(ButtonToastUX.ToastPadding)
make.centerY.equalTo(toast)
make.trailing.equalTo(button.snp.leading).offset(-ButtonToastUX.TitleButtonPadding)
}
}
button.snp.makeConstraints { (make) in
make.trailing.equalTo(toast).offset(-ButtonToastUX.ToastPadding)
make.centerY.equalTo(toast)
make.width.equalTo(button.titleLabel!.intrinsicContentSize.width + 2*ButtonToastUX.ToastButtonPadding)
}
return toast
}
fileprivate func dismiss(_ buttonPressed: Bool) {
guard dismissed == false else {
return
}
dismissed = true
superview?.removeGestureRecognizer(gestureRecognizer)
UIView.animate(withDuration: SimpleToastUX.ToastAnimationDuration, animations: {
self.animationConstraint?.update(offset: SimpleToastUX.ToastHeight)
self.layoutIfNeeded()
},
completion: { finished in
self.removeFromSuperview()
if !buttonPressed {
self.completionHandler?(false)
}
}
)
}
func showToast(duration: DispatchTimeInterval = SimpleToastUX.ToastDismissAfter) {
layoutIfNeeded()
UIView.animate(withDuration: SimpleToastUX.ToastAnimationDuration, animations: {
self.animationConstraint?.update(offset: 0)
self.layoutIfNeeded()
},
completion: { finished in
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
self.dismiss(false)
}
}
)
}
@objc func buttonPressed(_ gestureRecognizer: UIGestureRecognizer) {
self.completionHandler?(true)
self.dismiss(true)
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
superview?.addGestureRecognizer(gestureRecognizer)
}
func handleTap(_ gestureRecognizer: UIGestureRecognizer) {
dismiss(false)
}
}