Как представление может определить свою высоту на основе функции его ширины без использования intrinsicContentSize, который зависит от кадра? - PullRequest
1 голос
/ 29 мая 2020

Я пытаюсь понять, как лучше всего создать представление, которое определяет свою высоту на основе заданной ширины. Поведение, которое я желаю, очень похоже на то, как ведет себя вертикальный UIStackView:

  • Когда ограничено его верхним , ведущим и конечным краев, он должен определять свою собственную естественную высоту на основе его содержимого.
  • При ограничении всех краев он должен заполнять все доступное пространство, расширяясь и сворачивая, как определено этими ограничений.
  • Кроме того, я пытаюсь достичь этого без использования внутреннего автоматического раскладки для моего представления.

Чтобы проиллюстрировать это, рассмотрим следующий пример:

class ViewController: UIViewController {

    let v = View()

    override func viewDidLoad() {
        super.viewDidLoad()

        v.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(v)

        NSLayoutConstraint.activate([
            v.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            v.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            v.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            // Toggle this constraint on and off.
            // v.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        ])
    }
}

class View: UIView {

    // Imagine this came from computing the size of some child views
    // and that it is relative to the width of the bounds.
    var contentHeight: CGFloat { bounds.width * 0.5 }

    var boundsWidth: CGFloat = 0 {
        didSet { if oldValue != boundsWidth { invalidateIntrinsicContentSize() } }
    }

    override var intrinsicContentSize: CGSize {
        .init(width: bounds.width, height: contentHeight)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        boundsWidth = bounds.width
        backgroundColor = .red
    }
}

В этом примере достигается поведение, которое я описал выше, но у меня есть большая оговорка из-за того, как я использую intrinsicContentSize.

В документации для instrinsicContentSize указано, что не должно зависеть от фрейма содержимого , что мне не удалось. Я рассчитываю внутренний c размер на основе ширины кадра.

Как можно добиться такого поведения и не заставлять instrinsicContentSize полагаться на границы?

Ответы [ 2 ]

1 голос
/ 29 мая 2020

Все это можно сделать с помощью ограничений автоматического макета. Не нужно ничего вычислять.

Все, что вам нужно сделать, это убедиться, что содержимое вашего настраиваемого представления имеет соответствующие ограничения для определения макета.

Например, UILabel имеет внутренний c размер зависит от текста. Вы можете привязать «верхнюю» метку к верхней части вида, «среднюю» метку внизу «верхней» метки и «нижнюю» метку внизу «средней» метки и в нижнюю часть представления.

Вот пример (все через код):

class SizeTestViewController: UIViewController {

    let v = ExampleView()

    override func viewDidLoad() {
        super.viewDidLoad()

        v.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(v)

        NSLayoutConstraint.activate([
            v.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            v.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            v.trailingAnchor.constraint(equalTo: view.trailingAnchor),

        ])

        v.topLabel.text = "This is the top label."
        v.middleLabel.text = "This is a bunch of text for the middle label. Since we have it set to numberOfLines = 0, the text will wrap onto mutliple lines (assuming it needs to)."
        v.bottomLabel.text = "This is the bottom label text\nwith embedded newline characters\nso we can see the multiline feature without needing word wrap."

        // so we can see the view's frame
        v.backgroundColor = .red
    }
}

class ExampleView: UIView {
    var topLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .yellow
        v.numberOfLines = 0
        return v
    }()

    var middleLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .cyan
        v.numberOfLines = 0
        return v
    }()

    var bottomLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .green
        v.numberOfLines = 0
        return v
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {

        addSubview(topLabel)
        addSubview(middleLabel)
        addSubview(bottomLabel)

        NSLayoutConstraint.activate([

            // constrain topLabel 8-pts from top, leading, trailing
            topLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
            topLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            topLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),

            // constrain middleLabel 8-pts from topLabel
            //  8-pts from leading, trailing
            middleLabel.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 8.0),
            middleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            middleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),

            // constrain bottomLabel 8-pts from middleLabel
            //  8-pts from leading, trailing
            //  8-pts from bottom
            bottomLabel.topAnchor.constraint(equalTo: middleLabel.bottomAnchor, constant: 8.0),
            bottomLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            bottomLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
            bottomLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),

        ])

    }

}

Результат:

enter image description here

и повернут, так что вы можете увидеть автоматический размер:

enter image description here


Edit

Небольшое пояснение по Intrinsi c Content Size ...

На этом изображении все 5 подвидов имеют intrinsicContentSize из 120 x 80:

class IntrinsicTestView: UIView {
    override var intrinsicContentSize: CGSize {
        return CGSize(width: 120, height: 80)
    }
}

enter image description here

Как видите:

  • Если I не добавьте ограничения, чтобы указать Ширина - либо с ограничением ширины, либо с ограничениями в начале и конце - вид будет шириной 120 пунктов.
  • Если I don 't добавить ограничения, чтобы указать Высота - либо с ограничением высоты, либо сверху и снизу ограничения - вид будет иметь высоту 80 пунктов.
  • В противном случае ширина и высота будут определяться ограничениями, которые я добавил.

Вот полный код этого примера :

class IntrinsicTestView: UIView {
    override var intrinsicContentSize: CGSize {
        return CGSize(width: 120, height: 80)
    }
}

class IntrinsicExampleViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        var iViews: [IntrinsicTestView] = [IntrinsicTestView]()

        var v: IntrinsicTestView

        let colors: [UIColor] = [.red, .green, .blue, .yellow, .purple]

        colors.forEach { c in
            let v = IntrinsicTestView()
            v.backgroundColor = c
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            iViews.append(v)
        }

        let g = view.safeAreaLayoutGuide

        // first view at Top: 20 / Leading: 20
        v = iViews[0]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0).isActive = true
        v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true

        // second view at Top: 120 / Leading: 20
        //  height: 20
        v = iViews[1]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 120.0).isActive = true
        v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
        v.heightAnchor.constraint(equalToConstant: 20).isActive = true

        // third view at Top: 160 / Leading: 20
        //  height: 40 / width: 250
        v = iViews[2]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 160.0).isActive = true
        v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
        v.heightAnchor.constraint(equalToConstant: 40).isActive = true
        v.widthAnchor.constraint(equalToConstant: 250).isActive = true

        // fourth view at Top: 220
        //  trailing: 20 / width: 250
        v = iViews[3]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 220.0).isActive = true
        v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0).isActive = true
        v.widthAnchor.constraint(equalToConstant: 250).isActive = true

        // fourth view at Top: 400 / Leading: 20
        //  trailing: 20 / bottom: 20
        v = iViews[4]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 400.0).isActive = true
        v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
        v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0).isActive = true
        v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0).isActive = true

    }

}
0 голосов
/ 04 июня 2020

Как сказал ДонМаг в своем ответе, intrinsicContentSize не совсем предназначен для управления сложной компоновкой, а скорее как указание предпочтительного размера представления, когда для его фрейма недостаточно ограничений.

Однако есть это простой пример, который я могу предоставить, чтобы указать вам в правильном направлении.
Предположим, я хочу, чтобы RatioView имел внутренний размер, чтобы его высота была линейной функцией его ширины:

let ratioView = RatioView()
ratioView.ratio = 0.5

в этом примере ratioView будет иметь внутреннюю c высоту, равную половине его ширины.

Код для RatioView следующий:

class RatioView: UIView {
  var contentHeight: CGFloat { bounds.width * ratio }
  private var kvoObservation: NSKeyValueObservation?

  var ratio: CGFloat = 1 {
    didSet { invalidateIntrinsicContentSize() }
  }

  override var intrinsicContentSize: CGSize {
    .init(width: bounds.width, height: contentHeight)
  }

  override func didMoveToSuperview() {
    guard let _ = superview else { return }
    kvoObservation = observe(\.frame, options: .initial) { [weak self] (_, change) in
      self?.invalidateIntrinsicContentSize()
    }
  }

  override func willMove(toSuperview newSuperview: UIView?) {
    if newSuperview == nil {
      kvoObservation = nil
    }
  }
}

Там - два важных момента в приведенном выше коде:

  • представление вызывает invalidateIntrinsicContentSize, чтобы сообщить системе макета, что значение его свойства intrinsicContentSize изменилось.
  • вид постоянно отслеживается с помощью наблюдения KVO любых изменений его свойства frame, так что он может вычислять свою новую высоту каждый раз, когда изменяется его ширина.

seco -й шаг особенно примечателен: без него представление не узнало бы , когда его размер изменяется, и, следовательно, не было бы возможности сообщить системе макета (через invalidateIntrinsicContentSize), что его intrinsicContentSize был обновлен.

...