#swift #xcode #uibutton #uilabel #nslayoutconstraint
Вопрос:
Итак, у меня есть UIButton, и я устанавливаю в нем заголовок в строку, которая имеет динамическую длину. Я хочу, чтобы ширина titleLabel составляла половину ширины экрана. Я пытался использовать .sizeToFit(), но это приводит к тому, что кнопка использует CGSize до того, как ограничение было применено к titleLabel . Я пытался использовать .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() amp;amp; 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()
}
}
Вот как это выглядит для короткого заголовка и длинного заголовка. Когда заголовок становится слишком длинным, я хочу, чтобы текст был перенесен, а пузырь получил более высокую высоту. Как вы можете видеть на изображении, приведенном ниже, первое «короткое имя» отлично работает как пузырь аннотации карты. Однако, когда имя становится очень длинным, оно просто расширяет пузырь до такой степени, что он исчезает с экрана.
Любая помощь в исправлении приветствуется. Спасибо!
Комментарии:
1. Ваш вопрос довольно запутанный… Если заголовок вашей кнопки
Tap Me
, например, что вы подразумеваете под «Я хочу, чтобы ширина метки заголовка составляла половину ширины экрана» ? Можете ли вы добавить пару изображений, чтобы прояснить свою цель?2. @DonMag добавил изображения и поместил весь класс в текст вопроса, чтобы он был более полным.
3. Ах, вам нужна «многострочная кнопка»… была бы полезная информация при первой публикации вашего вопроса…
Ответ №1:
UIButton
Класс владеет titleLabel
и собирается позиционировать и устанавливать ограничения для этой метки самостоятельно. Скорее всего, вам придется создать подкласс UIButton
и переопределить его метод «updateConstraints», чтобы поместить titleLabel
его туда, куда вы хотите.
Вероятно, ваш код не должен основывать размер кнопки на размере экрана. Он может устанавливать размер какого-либо другого представления в вашей иерархии, которое соответствует размеру экрана, но захват границ экрана в середине настройки размера представления является необычным.
Комментарии:
1. Привет, Скотт, честно говоря, «ширина половины экрана» была произвольной. Ширина представления может быть некоторым фиксированным значением, например, 400 или около того. С этой спецификацией есть ли более простое решение, чем подкласс UIButton?
2. Не совсем… потому что кнопка владеет
titleLabel
и отвечает за ее позиционирование. Если вы не переопределите фреймворк, он это сделает.
Ответ №2:
Чтобы включить перенос слов в несколько строк в a UIButton
, вам необходимо создать свой собственный подкласс button .
Например:
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() amp;amp; 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)
}
}
Это выглядит так: