根据 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

将 UIButton 的 titleLabel 文本与自动布局匹配在固定的恒定宽度上

iOS 调整UIButton 图片(imageView)与文字(titleLabel)的位置

自动调整导航栏标题放错位置

UIButton:根据titleLabel Text计算UIButton的高度

在 TitleLabel 和 DetailLabel 上环绕文本 - 材质卡