根据 titleLabel 的长度调整 UIButton 的大小
Posted
技术标签:
【中文标题】根据 titleLabel 的长度调整 UIButton 的大小【英文标题】:Sizing UIButton depending on length of titleLabel 【发布时间】:2021-12-11 04:21:20 【问题描述】:所以我有一个 UIButton,我将其中的标题设置为一个动态长度的字符串。我希望 titleLabel 的宽度是屏幕宽度的一半。我尝试使用 .sizeToFit() 但这会导致按钮在将约束应用于 titleLabel 之前使用 CGSize。我尝试使用 .sizeThatFits(button.titleLabel?.intrinsicContentSize) 但这也没有用。我认为下面的重要函数是 init() 和 presentCallout(),但我展示整个类只是为了更全面地理解。我正在玩的课程看起来像:
class CustomCalloutView: UIView, MGLCalloutView
var representedObject: MGLAnnotation
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
override var center: CGPoint
set
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
get
return super.center
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
required init(representedObject: MGLAnnotation)
self.representedObject = representedObject
self.mainBody = UIButton(type: .system)
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
// I thought this would work, but it doesn't.
// mainBody.translatesAutoresizingMaskIntoConstraints = false
// mainBody.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
// mainBody.leftAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
required init?(coder decoder: NSCoder)
fatalError("init(coder:) has not been implemented")
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool)
delegate?.calloutViewWillAppear?(self)
view.addSubview(self)
// Prepare title label.
mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.titleLabel?.lineBreakMode = .byWordWrapping
mainBody.titleLabel?.numberOfLines = 0
mainBody.sizeToFit()
if isCalloutTappable()
// Handle taps and eventually try to send them to the delegate (usually the map view).
mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
else
// Disable tapping and highlighting.
mainBody.isUserInteractionEnabled = false
// Prepare our frame, adding extra space at the bottom for the tip.
let frameWidth = mainBody.bounds.size.width
let frameHeight = mainBody.bounds.size.height + tipHeight
let frameOriginX = rect.origin.x + (rect.size.width/2.0) - (frameWidth/2.0)
let frameOriginY = rect.origin.y - frameHeight
frame = CGRect(x: frameOriginX, y: frameOriginY, width: frameWidth, height: frameHeight)
if animated
alpha = 0
UIView.animate(withDuration: 0.2) [weak self] in
guard let strongSelf = self else
return
strongSelf.alpha = 1
strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
else
delegate?.calloutViewDidAppear?(self)
func dismissCallout(animated: Bool)
if (superview != nil)
if animated
UIView.animate(withDuration: 0.2, animations: [weak self] in
self?.alpha = 0
, completion: [weak self] _ in
self?.removeFromSuperview()
)
else
removeFromSuperview()
// MARK: - Callout interaction handlers
func isCalloutTappable() -> Bool
if let delegate = delegate
if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight))
return delegate.calloutViewShouldHighlight!(self)
return false
@objc func calloutTapped()
if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped))
delegate!.calloutViewTapped!(self)
// MARK: - Custom view styling
override func draw(_ rect: CGRect)
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .white
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
这就是短标题和长标题的样子。当标题太长时,我希望文本换行并且气泡得到更高的高度。正如您在下面的图片集中看到的那样,第一个“短名称”可以很好地用作地图注释气泡。但是,当名称变得超长时,它只会将气泡扩大到离开屏幕的程度。
https://imgur.com/a/I5z0zUd
非常感谢任何有关如何修复的帮助。谢谢!
【问题讨论】:
你的问题比较混乱...如果你的按钮标题是Tap Me
,例如,你的意思是什么"我希望titleLabel的宽度是屏幕的一半宽度”?您可以添加几张图片来阐明您的目标吗?
@DonMag 添加了图片并将整个课程放在问题的正文中,以便更全面。
啊 - 你想要一个“多行按钮”......在第一次发布你的问题时会提供有用的信息......
【参考方案1】:
UIButton
类拥有titleLabel
,并将定位并设置该标签本身的约束。您很可能必须创建UIButton
的子类并覆盖其“updateConstraints”方法以将titleLabel
定位到您想要的位置。
您的代码可能不应该根据屏幕大小来确定按钮的大小。它可能会设置层次结构中恰好是屏幕大小的其他视图的大小,但在设置视图大小的过程中抓取屏幕边界是不寻常的。
【讨论】:
嗨,Scott,老实说,“半屏宽度”是任意的。视图的宽度可以是某个固定值,例如 400 左右。有了这个规范,有没有比继承 UIButton 更简单的解决方案? 不是真的...因为按钮拥有titleLabel
并负责定位它。除非您覆盖框架,否则它会这样做。【参考方案2】:
要在UIButton
中启用多行自动换行,您需要创建自己的按钮子类。
例如:
class MultilineTitleButton: UIButton
required init?(coder aDecoder: NSCoder)
super.init(coder: aDecoder)
commonInit()
override init(frame: CGRect)
super.init(frame: frame)
commonInit()
func commonInit() -> Void
self.titleLabel?.numberOfLines = 0
self.titleLabel?.textAlignment = .center
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .vertical)
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .horizontal)
override var intrinsicContentSize: CGSize
let size = self.titleLabel!.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
override func layoutSubviews()
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
该按钮将标题包装成多行,配合自动布局/约束。
我没有任何使用 MapBox 的项目,但这里有一个使用 CustomCalloutView
的修改版本的示例。我注释掉了任何 MapBox 特定代码。您也许可以取消注释这些行并按原样使用:
class CustomCalloutView: UIView //, MGLCalloutView
//var representedObject: MGLAnnotation
var repTitle: String = ""
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
// NOTE: this causes a vertical shift when NOT using MapBox
// override var center: CGPoint
// set
// var newCenter = newValue
// newCenter.y -= bounds.midY
// super.center = newCenter
//
// get
// return super.center
//
//
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
//weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
var anchorView: UIView!
override func willMove(toSuperview newSuperview: UIView?)
if newSuperview == nil
anchorView.removeFromSuperview()
//required init(representedObject: MGLAnnotation)
required init(title: String)
self.repTitle = title
self.mainBody = MultilineTitleButton()
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.setTitleColor(.black, for: [])
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
mainBody.translatesAutoresizingMaskIntoConstraints = false
let padding: CGFloat = 8.0
NSLayoutConstraint.activate([
mainBody.topAnchor.constraint(equalTo: self.topAnchor, constant: padding),
mainBody.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding),
mainBody.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding),
mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding),
])
required init?(coder decoder: NSCoder)
fatalError("init(coder:) has not been implemented")
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool)
//delegate?.calloutViewWillAppear?(self)
// since we'll be using auto-layout for the mutli-line button
// we'll add an "anchor view" to the superview
// it will be removed when self is removed
anchorView = UIView(frame: rect)
anchorView.isUserInteractionEnabled = false
anchorView.backgroundColor = .clear
view.addSubview(anchorView)
view.addSubview(self)
// Prepare title label.
//mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.setTitle(self.repTitle, for: .normal)
// if isCalloutTappable()
// // Handle taps and eventually try to send them to the delegate (usually the map view).
// mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
// else
// // Disable tapping and highlighting.
// mainBody.isUserInteractionEnabled = false
//
self.translatesAutoresizingMaskIntoConstraints = false
anchorView.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin]
NSLayoutConstraint.activate([
self.centerXAnchor.constraint(equalTo: anchorView.centerXAnchor),
self.bottomAnchor.constraint(equalTo: anchorView.topAnchor),
self.widthAnchor.constraint(lessThanOrEqualToConstant: constrainedRect.width),
])
if animated
alpha = 0
UIView.animate(withDuration: 0.2) [weak self] in
guard let strongSelf = self else
return
strongSelf.alpha = 1
//strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
else
//delegate?.calloutViewDidAppear?(self)
func dismissCallout(animated: Bool)
if (superview != nil)
if animated
UIView.animate(withDuration: 0.2, animations: [weak self] in
self?.alpha = 0
, completion: [weak self] _ in
self?.removeFromSuperview()
)
else
removeFromSuperview()
// MARK: - Callout interaction handlers
// func isCalloutTappable() -> Bool
// if let delegate = delegate
// if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight))
// return delegate.calloutViewShouldHighlight!(self)
//
//
// return false
//
//
// @objc func calloutTapped()
// if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped))
// delegate!.calloutViewTapped!(self)
//
//
// MARK: - Custom view styling
override func draw(_ rect: CGRect)
print(#function)
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .red
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
这是一个示例视图控制器,显示具有各种长度标题的“标注视图”,限制为视图宽度的 70%:
class CalloutTestVC: UIViewController
let sampleTitles: [String] = [
"Short Title",
"Slightly Longer Title",
"A ridiculously long title that will need to wrap!",
]
var idx: Int = -1
let tapView = UIView()
var ccv: CustomCalloutView!
override func viewDidLoad()
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.8939146399, green: 0.8417750597, blue: 0.7458069921, alpha: 1)
tapView.backgroundColor = .systemBlue
tapView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tapView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tapView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
tapView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
tapView.widthAnchor.constraint(equalToConstant: 60),
tapView.heightAnchor.constraint(equalTo: tapView.widthAnchor),
])
// tap the Blue View to cycle through Sample Titles for the Callout View
// using the Blue view as the "anchor rect"
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap))
tapView.addGestureRecognizer(t)
@objc func gotTap() -> Void
if ccv != nil
ccv.removeFromSuperview()
// increment sampleTitles array index
// to cycle through the strings
idx += 1
let validIdx = idx % sampleTitles.count
let str = sampleTitles[validIdx]
// create a new Callout view
ccv = CustomCalloutView(title: str)
// to restrict the "callout view" width to less-than 1/2 the screen width
// use view.width * 0.5 for the constrainedTo width
// may look better restricting it to 70%
ccv.presentCallout(from: tapView.frame, in: self.view, constrainedTo: CGRect(x: 0, y: 0, width: view.frame.size.width * 0.7, height: 100), animated: false)
看起来像这样:
【讨论】:
以上是关于根据 titleLabel 的长度调整 UIButton 的大小的主要内容,如果未能解决你的问题,请参考以下文章
将 UIButton 的 titleLabel 文本与自动布局匹配在固定的恒定宽度上
iOS 调整UIButton 图片(imageView)与文字(titleLabel)的位置