创建可重用的自定义按钮视图的最佳做法是什么?
What is best practice for creating a reusable custom button views?
我下面有三个按钮具有相同的 UI,唯一的区别是标签的文本和点击手势操作。它看起来像这样:
根据这种情况创建可重复使用的自定义按钮视图的最佳做法是什么?
到目前为止,我尝试使用:(1) 自定义按钮 class 但难以实现堆栈视图,我可以在该视图中配置按钮中的两个标签,(2) UI 按钮扩展但是点击按钮导致应用程序崩溃的问题
class SetActivityVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
lazy var firstButton: UIButton = {
let button = UIButton()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapFirst))
button.addGestureRecognizer(tapGesture)
button.setBackgroundImage(Image.setButtonBg, for: .normal)
button.addShadowEffect()
let label = UILabel()
label.text = "No Exercise"
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textColor = .black
let subLabel = UILabel()
subLabel.text = "no exercise or very infrequent"
subLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
subLabel.textColor = .gray
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
stack.isUserInteractionEnabled = true
stack.addGestureRecognizer(tapGesture)
button.addSubview(stack)
stack.centerInSuperview()
return button
}()
lazy var secondButton: UIButton = {
let button = UIButton()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapSecond))
button.addGestureRecognizer(tapGesture)
button.setBackgroundImage(Image.setButtonBg, for: .normal)
button.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
button.addShadowEffect()
let label = UILabel()
label.text = "Light Exercise"
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textColor = .black
let subLabel = UILabel()
subLabel.text = "some light cardio/weights a few times per week"
subLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
subLabel.textColor = .gray
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
button.addSubview(stack)
stack.centerInSuperview()
return button
}()
lazy var thirdButton: UIButton = {
let button = UIButton()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapThird))
button.addGestureRecognizer(tapGesture)
button.setBackgroundImage(Image.setButtonBg, for: .normal)
button.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
button.addShadowEffect()
let label = UILabel()
label.text = "Moderate Exercise"
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textColor = .black
let subLabel = UILabel()
subLabel.text = "lifting/cardio regularly but not super intense"
subLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
subLabel.textColor = .gray
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
button.addSubview(stack)
stack.centerInSuperview()
return button
}()
@objc func didTapFirst() {
print("Tapped 1")
}
@objc func didTapSecond() {
print("Tapped 2")
}
@objc func didTapThird() {
print("Tapped 3")
}
}
extension SetActivityVC {
fileprivate func setupViews() {
addViews()
constrainViews()
}
fileprivate func addViews() {
view.addSubview(firstButton)
view.addSubview(secondButton)
view.addSubview(thirdButton)
}
// Using TinyConstraints
fileprivate func constrainViews() {
firstButton.centerXToSuperview()
secondButton.centerXToSuperview()
secondButton.topToBottom(of: firstButton, offset: screenHeight * 0.03)
thirdButton.centerXToSuperview()
thirdButton.topToBottom(of: secondButton, offset: screenHeight * 0.03)
}
}
没有通用的答案,因为每种情况都是独一无二的,但通常有几种常见的模式:
- 实现一个工厂方法来创建一个按钮,设置它的所有属性并return它。
- 子类
UIButton
并添加新行为和合理的默认值。
- 子类
UIControl
用于完全自定义的内容,例如由多个其他视图组成的控件。
现在,您的特定问题似乎是实现一个可重复使用的按钮,其中包含两行不同样式的文本。
将标签作为子视图添加到 UIButton
是我绝对不会推荐的。这会破坏可访问性,您将不得不做很多工作来支持不同的按钮状态,例如突出显示或禁用。
相反,我强烈建议您使用 UIButton
的一个很棒的功能:它支持标题的属性字符串,而且标题也可以是多行的,因为您可以访问按钮的 titleLabel
属性.
子类化 UIButton
只是为了合理的默认值和易于设置似乎是一个不错的选择:
struct TwoLineButtonModel {
let title: String
let subtitle: String
let action: () -> Void
}
final class TwoLineButton: UIButton {
private var action: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
addTarget(self, action: #selector(handleTap(_:)), for: .touchUpInside)
setUpAppearance()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with model: TwoLineButtonModel) {
[.normal, .highlighted, .disabled].forEach {
setAttributedTitle(
makeButtonTitle(
title: model.title,
subtitle: model.subtitle,
forState: [=10=]
),
for: [=10=]
)
}
action = model.action
}
@objc private func handleTap(_ sender: Any) {
action?()
}
private func setUpAppearance() {
backgroundColor = .yellow
layer.cornerRadius = 16
titleLabel?.numberOfLines = 0
contentEdgeInsets = UIEdgeInsets(top: 16, left: 8, bottom: 16, right: 8)
}
private func makeButtonTitle(
title: String,
subtitle: String,
forState state: UIControl.State
) -> NSAttributedString {
let centeredParagraphStyle = NSMutableParagraphStyle()
centeredParagraphStyle.alignment = .center
let primaryColor: UIColor = {
switch state {
case .highlighted:
return .label.withAlphaComponent(0.5)
case .disabled:
return .label.withAlphaComponent(0.3)
default:
return .label
}
}()
let secondaryColor: UIColor = {
switch state {
case .highlighted:
return .secondaryLabel.withAlphaComponent(0.3)
case .disabled:
return .secondaryLabel.withAlphaComponent(0.1)
default:
return .secondaryLabel
}
}()
let parts = [
NSAttributedString(
string: title + "\n",
attributes: [
.font: UIFont.preferredFont(forTextStyle: .title1),
.foregroundColor: primaryColor,
.paragraphStyle: centeredParagraphStyle
]
),
NSAttributedString(
string: subtitle,
attributes: [
.font: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: secondaryColor,
.paragraphStyle: centeredParagraphStyle
]
)
]
let string = NSMutableAttributedString()
parts.forEach { string.append([=10=]) }
return string
}
}
我的示例中的文本样式和颜色可能与您的需要不完全匹配,但它很容易调整,您可以从这里获取。将应该可定制的东西移到视图模型中,同时将合理的默认值保留为私有实现。如果您还不熟悉 NSAttributedString
上的教程,它会为您提供很大的文本样式设置自由度。
我下面有三个按钮具有相同的 UI,唯一的区别是标签的文本和点击手势操作。它看起来像这样:
根据这种情况创建可重复使用的自定义按钮视图的最佳做法是什么?
到目前为止,我尝试使用:(1) 自定义按钮 class 但难以实现堆栈视图,我可以在该视图中配置按钮中的两个标签,(2) UI 按钮扩展但是点击按钮导致应用程序崩溃的问题
class SetActivityVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
lazy var firstButton: UIButton = {
let button = UIButton()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapFirst))
button.addGestureRecognizer(tapGesture)
button.setBackgroundImage(Image.setButtonBg, for: .normal)
button.addShadowEffect()
let label = UILabel()
label.text = "No Exercise"
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textColor = .black
let subLabel = UILabel()
subLabel.text = "no exercise or very infrequent"
subLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
subLabel.textColor = .gray
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
stack.isUserInteractionEnabled = true
stack.addGestureRecognizer(tapGesture)
button.addSubview(stack)
stack.centerInSuperview()
return button
}()
lazy var secondButton: UIButton = {
let button = UIButton()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapSecond))
button.addGestureRecognizer(tapGesture)
button.setBackgroundImage(Image.setButtonBg, for: .normal)
button.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
button.addShadowEffect()
let label = UILabel()
label.text = "Light Exercise"
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textColor = .black
let subLabel = UILabel()
subLabel.text = "some light cardio/weights a few times per week"
subLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
subLabel.textColor = .gray
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
button.addSubview(stack)
stack.centerInSuperview()
return button
}()
lazy var thirdButton: UIButton = {
let button = UIButton()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapThird))
button.addGestureRecognizer(tapGesture)
button.setBackgroundImage(Image.setButtonBg, for: .normal)
button.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
button.addShadowEffect()
let label = UILabel()
label.text = "Moderate Exercise"
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textColor = .black
let subLabel = UILabel()
subLabel.text = "lifting/cardio regularly but not super intense"
subLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
subLabel.textColor = .gray
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
button.addSubview(stack)
stack.centerInSuperview()
return button
}()
@objc func didTapFirst() {
print("Tapped 1")
}
@objc func didTapSecond() {
print("Tapped 2")
}
@objc func didTapThird() {
print("Tapped 3")
}
}
extension SetActivityVC {
fileprivate func setupViews() {
addViews()
constrainViews()
}
fileprivate func addViews() {
view.addSubview(firstButton)
view.addSubview(secondButton)
view.addSubview(thirdButton)
}
// Using TinyConstraints
fileprivate func constrainViews() {
firstButton.centerXToSuperview()
secondButton.centerXToSuperview()
secondButton.topToBottom(of: firstButton, offset: screenHeight * 0.03)
thirdButton.centerXToSuperview()
thirdButton.topToBottom(of: secondButton, offset: screenHeight * 0.03)
}
}
没有通用的答案,因为每种情况都是独一无二的,但通常有几种常见的模式:
- 实现一个工厂方法来创建一个按钮,设置它的所有属性并return它。
- 子类
UIButton
并添加新行为和合理的默认值。 - 子类
UIControl
用于完全自定义的内容,例如由多个其他视图组成的控件。
现在,您的特定问题似乎是实现一个可重复使用的按钮,其中包含两行不同样式的文本。
将标签作为子视图添加到 UIButton
是我绝对不会推荐的。这会破坏可访问性,您将不得不做很多工作来支持不同的按钮状态,例如突出显示或禁用。
相反,我强烈建议您使用 UIButton
的一个很棒的功能:它支持标题的属性字符串,而且标题也可以是多行的,因为您可以访问按钮的 titleLabel
属性.
子类化 UIButton
只是为了合理的默认值和易于设置似乎是一个不错的选择:
struct TwoLineButtonModel {
let title: String
let subtitle: String
let action: () -> Void
}
final class TwoLineButton: UIButton {
private var action: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
addTarget(self, action: #selector(handleTap(_:)), for: .touchUpInside)
setUpAppearance()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with model: TwoLineButtonModel) {
[.normal, .highlighted, .disabled].forEach {
setAttributedTitle(
makeButtonTitle(
title: model.title,
subtitle: model.subtitle,
forState: [=10=]
),
for: [=10=]
)
}
action = model.action
}
@objc private func handleTap(_ sender: Any) {
action?()
}
private func setUpAppearance() {
backgroundColor = .yellow
layer.cornerRadius = 16
titleLabel?.numberOfLines = 0
contentEdgeInsets = UIEdgeInsets(top: 16, left: 8, bottom: 16, right: 8)
}
private func makeButtonTitle(
title: String,
subtitle: String,
forState state: UIControl.State
) -> NSAttributedString {
let centeredParagraphStyle = NSMutableParagraphStyle()
centeredParagraphStyle.alignment = .center
let primaryColor: UIColor = {
switch state {
case .highlighted:
return .label.withAlphaComponent(0.5)
case .disabled:
return .label.withAlphaComponent(0.3)
default:
return .label
}
}()
let secondaryColor: UIColor = {
switch state {
case .highlighted:
return .secondaryLabel.withAlphaComponent(0.3)
case .disabled:
return .secondaryLabel.withAlphaComponent(0.1)
default:
return .secondaryLabel
}
}()
let parts = [
NSAttributedString(
string: title + "\n",
attributes: [
.font: UIFont.preferredFont(forTextStyle: .title1),
.foregroundColor: primaryColor,
.paragraphStyle: centeredParagraphStyle
]
),
NSAttributedString(
string: subtitle,
attributes: [
.font: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: secondaryColor,
.paragraphStyle: centeredParagraphStyle
]
)
]
let string = NSMutableAttributedString()
parts.forEach { string.append([=10=]) }
return string
}
}
我的示例中的文本样式和颜色可能与您的需要不完全匹配,但它很容易调整,您可以从这里获取。将应该可定制的东西移到视图模型中,同时将合理的默认值保留为私有实现。如果您还不熟悉 NSAttributedString
上的教程,它会为您提供很大的文本样式设置自由度。