如何在 UIKit 中使用堆栈视图创建可重用按钮 class?
How to create a reusable button class with a stack view in UIKit?
我正在使用 class 构建一个可重复使用的按钮(下图),它使用堆栈视图垂直放置两个标签,并允许我在调用时配置两个标签的文本。
我尝试在下面的 init 方法中将“label”和“subLabel”添加到 UIStackView 中,但堆栈没有添加到按钮的视图中。
将堆栈视图集成到此自定义按钮中的最佳方法是什么class?
struct ActivityButtonVM {
let labelText: String
let subLabelText: String
let action: Selector
}
final class ActivityButton: UIButton {
private let label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
return label
}()
private let subLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .gray
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setBackgroundImage(Image.setButtonBg, for: .normal)
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
addSubview(stack)
clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with viewModel: ActivityButtonVM) {
label.text = viewModel.labelText
subLabel.text = viewModel.subLabelText
self.addTarget(SetActivityVC(), action: viewModel.action,
for: .touchUpInside)
}
}
这就是我使用此自定义按钮的方式 class:
class SetActivityVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
lazy var firstButton: UIButton = {
let button = ActivityButton()
button.configure(with: ActivityButtonVM(labelText: "No Exercise", subLabelText: "no exercise or very infrequent", action: #selector(didTapFirst))
return button
}()
lazy var secondButton: UIButton = {
let button = ActivityButton()
button.configure(with: ActivityButtonVM(labelText: "Light Exercise", subLabelText: "some light cardio/weights a few times per week", action: #selector(didTapSecond))
return button
}()
@objc func didTapFirst() {
print("Tapped 1")
}
@objc func didTapSecond() {
print("Tapped 2")
}
}
extension SetActivityVC {
fileprivate func setupViews() {
addViews()
constrainViews()
}
fileprivate func addViews() {
view.addSubview(firstButton)
view.addSubview(secondButton)
}
fileprivate func constrainViews() {
firstButton.centerXToSuperview()
secondButton.centerXToSuperview()
secondButton.topToBottom(of: firstButton, offset: screenHeight * 0.03)
}
}
首先,您在初始化按钮时没有调用 init(frame:)
:
let button = ActivityButton()
您只是调用从 NSObject
继承的初始化程序,因此当然不会添加堆栈视图。
您可以自己添加一个无参数的便利初始化程序,调用 self.init(frame:)
:
convenience init() {
self.init(frame: .zero)
}
然后将添加堆栈视图。
我认为您还需要添加:
stack.translatesAutoresizingMaskIntoConstraints = false
阻止自动调整掩码约束导致堆栈视图具有 .zero
框架。
此外,您应该向堆栈视图添加约束,以便它相对于按钮正确定位。 (可能将 4 个边固定到按钮的 4 个边上?)
最后但同样重要的是,您添加目标的方式不正确。您要在此处添加 SetActivityVC
的 新实例 作为目标,而不是具有按钮的 VC 的实例。
self.addTarget(SetActivityVC(), action: viewModel.action,
for: .touchUpInside)
相反,如果您想使用 target-action 对执行此操作,您还应该在视图模型中包含 target
:
struct ActivityButtonVM {
let labelText: String
let subLabelText: String
let target: Any // <----
let action: Selector
}
...
self.addTarget(viewModel.target, action: viewModel.action,
for: .touchUpInside)
提示:不要使用 .black
和 .gray
等颜色,而是使用 .label
和 .secondaryLabel
这样它在深色模式下也看起来不错。
您可以使用替代方法:新 UIButton.configuration,声明您的按钮:
let myButton1 = UIButton()
let myButton2 = UIButton()
let myButton3 = UIButton()
let myButton4 = UIButton()
现在为按钮配置添加此扩展:
extension UIViewController {
func buttonConfiguration(button: UIButton, config: UIButton.Configuration, title: String, subtitle: String, bgColor: UIColor, foregColor: UIColor, imageSystemName: String, imageTintColor: UIColor) {
let b = button
b.configuration = config
b.configuration?.title = title
b.configuration?.titleAlignment = .center
b.configuration?.subtitle = subtitle
b.configuration?.baseForegroundColor = foregColor
b.configuration?.baseBackgroundColor = bgColor
b.configuration?.image = UIImage(systemName: imageSystemName)?.withTintColor(imageTintColor, renderingMode: .alwaysOriginal)
b.configuration?.imagePlacement = .top
b.configuration?.imagePadding = 6
b.configuration?.cornerStyle = .large
}
如何使用,在viewDidLoad中设置你的按钮和相关目标:
buttonConfiguration(button: myButton1, config: .filled(), title: "My Button One", subtitle: "This is first button", bgColor: colorUpGradient, foregColor: .white, imageSystemName: "sun.min", imageTintColor: .orange)
myButton1.addTarget(self, action: #selector(didTapFirst), for: .touchUpInside)
buttonConfiguration(button: myButton2, config: .filled(), title: "My Button Two", subtitle: "This is second button", bgColor: .fuxiaRed, foregColor: .white, imageSystemName: "cloud", imageTintColor: .white)
myButton2.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
buttonConfiguration(button: myButton3, config: .filled(), title: "My Button Tree", subtitle: "This is third button", bgColor: .celesteCiopChiaro, foregColor: .black, imageSystemName: "cloud.drizzle", imageTintColor: .red)
myButton3.addTarget(self, action: #selector(didTapThird), for: .touchUpInside)
buttonConfiguration(button: myButton4, config: .filled(), title: "My Button Four", subtitle: "This is four button", bgColor: .darkYellow, foregColor: .black, imageSystemName: "cloud.bolt", imageTintColor: .black)
myButton4.addTarget(self, action: #selector(didTapFour), for: .touchUpInside)
设置您的堆栈视图和约束:
let stackView = UIStackView(arrangedSubviews: [myButton1, myButton2, myButton3, myButton4])
stackView.axis = .vertical
stackView.spacing = 12
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 372).isActive = true // 84(height of single button) * 4(number of buttons) = 336 + 36(total stackView spaces from buttons) = 372(height of intere stackView)
stackView.widthAnchor.constraint(equalToConstant: view.frame.width - 60).isActive = true // set width of button
添加按钮功能:
@objc func didTapFirst() {
print("Tapped 1")
}
@objc func didTapSecond() {
print("Tapped 2")
}
@objc func didTapThird() {
print("Tapped 3")
}
@objc func didTapFour() {
print("Tapped 4")
}
这是结果:
我正在使用 class 构建一个可重复使用的按钮(下图),它使用堆栈视图垂直放置两个标签,并允许我在调用时配置两个标签的文本。
我尝试在下面的 init 方法中将“label”和“subLabel”添加到 UIStackView 中,但堆栈没有添加到按钮的视图中。
将堆栈视图集成到此自定义按钮中的最佳方法是什么class?
struct ActivityButtonVM {
let labelText: String
let subLabelText: String
let action: Selector
}
final class ActivityButton: UIButton {
private let label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
return label
}()
private let subLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .gray
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setBackgroundImage(Image.setButtonBg, for: .normal)
let stack = UIStackView(arrangedSubviews: [label, subLabel])
stack.axis = .vertical
stack.alignment = .center
addSubview(stack)
clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with viewModel: ActivityButtonVM) {
label.text = viewModel.labelText
subLabel.text = viewModel.subLabelText
self.addTarget(SetActivityVC(), action: viewModel.action,
for: .touchUpInside)
}
}
这就是我使用此自定义按钮的方式 class:
class SetActivityVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
lazy var firstButton: UIButton = {
let button = ActivityButton()
button.configure(with: ActivityButtonVM(labelText: "No Exercise", subLabelText: "no exercise or very infrequent", action: #selector(didTapFirst))
return button
}()
lazy var secondButton: UIButton = {
let button = ActivityButton()
button.configure(with: ActivityButtonVM(labelText: "Light Exercise", subLabelText: "some light cardio/weights a few times per week", action: #selector(didTapSecond))
return button
}()
@objc func didTapFirst() {
print("Tapped 1")
}
@objc func didTapSecond() {
print("Tapped 2")
}
}
extension SetActivityVC {
fileprivate func setupViews() {
addViews()
constrainViews()
}
fileprivate func addViews() {
view.addSubview(firstButton)
view.addSubview(secondButton)
}
fileprivate func constrainViews() {
firstButton.centerXToSuperview()
secondButton.centerXToSuperview()
secondButton.topToBottom(of: firstButton, offset: screenHeight * 0.03)
}
}
首先,您在初始化按钮时没有调用 init(frame:)
:
let button = ActivityButton()
您只是调用从 NSObject
继承的初始化程序,因此当然不会添加堆栈视图。
您可以自己添加一个无参数的便利初始化程序,调用 self.init(frame:)
:
convenience init() {
self.init(frame: .zero)
}
然后将添加堆栈视图。
我认为您还需要添加:
stack.translatesAutoresizingMaskIntoConstraints = false
阻止自动调整掩码约束导致堆栈视图具有 .zero
框架。
此外,您应该向堆栈视图添加约束,以便它相对于按钮正确定位。 (可能将 4 个边固定到按钮的 4 个边上?)
最后但同样重要的是,您添加目标的方式不正确。您要在此处添加 SetActivityVC
的 新实例 作为目标,而不是具有按钮的 VC 的实例。
self.addTarget(SetActivityVC(), action: viewModel.action,
for: .touchUpInside)
相反,如果您想使用 target-action 对执行此操作,您还应该在视图模型中包含 target
:
struct ActivityButtonVM {
let labelText: String
let subLabelText: String
let target: Any // <----
let action: Selector
}
...
self.addTarget(viewModel.target, action: viewModel.action,
for: .touchUpInside)
提示:不要使用 .black
和 .gray
等颜色,而是使用 .label
和 .secondaryLabel
这样它在深色模式下也看起来不错。
您可以使用替代方法:新 UIButton.configuration,声明您的按钮:
let myButton1 = UIButton()
let myButton2 = UIButton()
let myButton3 = UIButton()
let myButton4 = UIButton()
现在为按钮配置添加此扩展:
extension UIViewController {
func buttonConfiguration(button: UIButton, config: UIButton.Configuration, title: String, subtitle: String, bgColor: UIColor, foregColor: UIColor, imageSystemName: String, imageTintColor: UIColor) {
let b = button
b.configuration = config
b.configuration?.title = title
b.configuration?.titleAlignment = .center
b.configuration?.subtitle = subtitle
b.configuration?.baseForegroundColor = foregColor
b.configuration?.baseBackgroundColor = bgColor
b.configuration?.image = UIImage(systemName: imageSystemName)?.withTintColor(imageTintColor, renderingMode: .alwaysOriginal)
b.configuration?.imagePlacement = .top
b.configuration?.imagePadding = 6
b.configuration?.cornerStyle = .large
}
如何使用,在viewDidLoad中设置你的按钮和相关目标:
buttonConfiguration(button: myButton1, config: .filled(), title: "My Button One", subtitle: "This is first button", bgColor: colorUpGradient, foregColor: .white, imageSystemName: "sun.min", imageTintColor: .orange)
myButton1.addTarget(self, action: #selector(didTapFirst), for: .touchUpInside)
buttonConfiguration(button: myButton2, config: .filled(), title: "My Button Two", subtitle: "This is second button", bgColor: .fuxiaRed, foregColor: .white, imageSystemName: "cloud", imageTintColor: .white)
myButton2.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
buttonConfiguration(button: myButton3, config: .filled(), title: "My Button Tree", subtitle: "This is third button", bgColor: .celesteCiopChiaro, foregColor: .black, imageSystemName: "cloud.drizzle", imageTintColor: .red)
myButton3.addTarget(self, action: #selector(didTapThird), for: .touchUpInside)
buttonConfiguration(button: myButton4, config: .filled(), title: "My Button Four", subtitle: "This is four button", bgColor: .darkYellow, foregColor: .black, imageSystemName: "cloud.bolt", imageTintColor: .black)
myButton4.addTarget(self, action: #selector(didTapFour), for: .touchUpInside)
设置您的堆栈视图和约束:
let stackView = UIStackView(arrangedSubviews: [myButton1, myButton2, myButton3, myButton4])
stackView.axis = .vertical
stackView.spacing = 12
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 372).isActive = true // 84(height of single button) * 4(number of buttons) = 336 + 36(total stackView spaces from buttons) = 372(height of intere stackView)
stackView.widthAnchor.constraint(equalToConstant: view.frame.width - 60).isActive = true // set width of button
添加按钮功能:
@objc func didTapFirst() {
print("Tapped 1")
}
@objc func didTapSecond() {
print("Tapped 2")
}
@objc func didTapThird() {
print("Tapped 3")
}
@objc func didTapFour() {
print("Tapped 4")
}
这是结果: