Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Магия UILabel или приватное API Autolayout

CocoaHeads
April 26, 2018
240

Магия UILabel или приватное API Autolayout

CocoaHeads

April 26, 2018
Tweet

More Decks by CocoaHeads

Transcript

  1. План ▌ Вспомнить UILabel ▌ Попытаться повторить UILabel ▌ Разобраться

    с Autolayout › Private API › Инструменты & подходы ▌ Воспроивести UILabel 3
  2. UILabel 6 Ожидаемое и знакомое поведение Но как UILabel это

    делает? ▌ Автоматический расчет высоты, ▌ основанный на заданных ▌ констрейнтах и контенте
  3. › UIView › Рендеринг через CATextLayer (или CoreText, или еще

    как угодно) › Тривиальный интерфейс var attributedText: NSAttributedString? { get set } › Self-sizing поведение как у UILabel › Работать везде, где работает UILabel › Не требовать написания дополнительного кода AttributedStringLabel - ASLabel 8
  4. Хелпер 9 extension NSAttributedString { func size(forWidth width: CGFloat) ->

    CGSize { return boundingRect( with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil ).size } } ▌ Получение высоты контента NSAttributedString ▌ в рамках заданной ширины
  5. intrinsicContentSize 10 ▌ Связь нашего контента с миром Autolayout intrinsicContentSize

    = (500.0, 200.0) .widthAnchor (const = 500.0) .heightAnchor (const = 200.0)
  6. override var intrinsicContentSize: CGSize { return attributedText?.size(forWidth: bounds.width) ?? .zero

    } override func layoutSubviews() { super.layoutSubviews() if savedBounds == nil || savedBounds != bounds { invalidateIntrinsicContentSize() } savedBounds = bounds } private var savedBounds: CGRect? intrinsicContentSize 12
  7. override var intrinsicContentSize: CGSize { return attributedText?.size(forWidth: bounds.width) ?? .zero

    } override func layoutSubviews() { super.layoutSubviews() if savedBounds == nil || savedBounds != bounds { invalidateIntrinsicContentSize() } savedBounds = bounds } private var savedBounds: CGRect? intrinsicContentSize 13
  8. override var intrinsicContentSize: CGSize { return attributedText?.size(forWidth: bounds.width) ?? .zero

    } override func layoutSubviews() { super.layoutSubviews() if savedBounds == nil || savedBounds != bounds { invalidateIntrinsicContentSize() } savedBounds = bounds } private var savedBounds: CGRect? intrinsicContentSize 14
  9. intrinsicContentSize 16 ASLabel UILabel › UITableView › UITableViewAutomaticDimension › Контент

    в ячейке прибит констрейнтами к краям
  10. intrinsicContentSize 17 ▌ Что-то пошло не так! UITableView + UITableViewAutomaticDimension

    = Некорректный размер ячейки с ASLabel Но UILabel работает верно!
  11. Высота ячейки height Система сама считает высоту через systemLayoutSizeFitting tableView(_:heightForRowAt:)

    18 height == Auto? Нет Да height *Auto = UITableViewAutomaticDimension systemLayoutSizeFitting(_:)
  12. 19 Нет возможности верно рассчитать intrinsicContentSize в текущей реализации ASLabel

    systemLayoutSizeFitting(_:) › Не влияет на размер UIView, не обновляет bounds › Не вызывает layoutSubviews
  13. 20 func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize ▌ Система отдает

    нам targetSize! › Определим systemLayoutSizeFitting у UITableViewCell › Прокинем targetSize в контент ячейки › Используем его при расчете intrinsicContentSize systemLayoutSizeFitting(_:)
  14. systemLayoutSizeFitting(_:) 21 ASLabel View-Tree dx2 dx3 LabelTargetSize TargetSize dy1 dy2

    LabelTargetSize = CGSize( width: TargetSize – dx1 – dx2 – dx3 – image.size.width, height: TargetSize – dy1 – dy2 ) Image dx1
  15. // Cell.swift override func systemLayoutSizeFitting(...) -> CGSize { // Прокинуть

    верный размер с учетом отступов (content as? ASLabel)?.fittingSize = computeContentTargetSize(for: targetSize) return super.systemLayoutSizeFitting(...) } // ASLabel.swift override var intrinsicContentSize: CGSize { return attributedText?.size( forWidth: (fittingSize ?? bounds).width ) ?? .zero } var fittingSize: CGRect? 22
  16. // Cell.swift override func systemLayoutSizeFitting(...) -> CGSize { // Прокинуть

    верный размер с учетом отступов (content as? ASLabel)?.fittingSize = computeContentTargetSize(for: targetSize) return super.systemLayoutSizeFitting(...) } // ASLabel.swift override var intrinsicContentSize: CGSize { return attributedText?.size( forWidth: (fittingSize ?? bounds).width ) ?? .zero } var fittingSize: CGRect? 23
  17. ASLabel 25 Получили рабочее решение! › Переопределение systemLayoutSizeFitting › Необходимость

    протаскивания логики работы с ASLabel по всему View-Tree Но не такое удобное как UILabel
  18. Разбираемся с UILabel 26 ▌ Надо знать опорную ширину в

    момент расчета intrinsicContentSize ASLabel: › второй layout-pass благодаря invalidateIntrinsicContentSize в layoutSubviews › targetSize из systemLayoutSizeFitting UILabel:
  19. Разбираемся с UILabel (Поиск API) 28 Надо получить опорную ширину

    è Нужен как минимум один layout-pass Предположение Делаем еще один layout-pass с верным intrinsicContentSize è Задачу можно решить минимум за 2 прохода лейаута Зная опорную ширину расчитываем intrinsicContentSize
  20. Разбираемся с UILabel (Поиск API) 29 RuntimeBrowser › UIView.h ›

    UILabel.h › … _prepareForFirstIntrinsicContentSizeCalculation _prepareForSecondIntrinsicContentSizeCalculationWithLayoutEngineBounds
  21. Разбираемся с UILabel (Поиск API) 30 У UIView не вызывается,

    у UILabel - да! ▌ Система явно подсказывает bounds для расчета IntrinsicContentSize ▌ Предположение про два прохода подтверждается @objc func _prepareForSecondIntrinsicContentSizeCalculation( withLayoutEngineBounds bounds: CGRect)
  22. Разбираемся с UILabel (Поиск API) 31 Xtrace! [<CustomLabel 0x7fcb8350d0d0>/UILabel _prepareForFirstIntrinsicContentSizeCalculation]

    ... [<CustomLabel 0x7fcb8350d0d0>/UILabel _needsDoubleUpdateConstraintsPass] -> 1 ... [<CustomLabel 0x7fcb8350d0d0>/UILabel _prepareForSecondIntrinsicContentSizeCalculationWithLayoutEngineBounds: {{20, 20}, {25000000, 20.5}}]
  23. [<CustomLabel 0x7fcb8350d0d0>/UILabel _prepareForFirstIntrinsicContentSizeCalculation] ... [<CustomLabel 0x7fcb8350d0d0>/UILabel _needsDoubleUpdateConstraintsPass] -> 1 ...

    [<CustomLabel 0x7fcb8350d0d0>/UILabel _prepareForSecondIntrinsicContentSizeCalculationWithLayoutEngineBounds: {{20, 20}, {25000000, 20.5}}] Разбираемся с UILabel (Поиск API) 32 Xtrace! Бинго!
  24. Разбираемся с UILabel (Поиск API) 33 Бинго! func _needsDoubleUpdateConstraintsPass() ->

    Bool ▌ Намекающее название У UIView - false, у UILabel - true!
  25. Разбираемся с UILabel (Поиск API) 34 Бинго! [<CustomLabel 0x7fcb8350d0d0>/UILabel _needsDoubleUpdateConstraintsPass]

    -> 1 ... [<CustomLabel 0x7fcb8350d0d0>/UIView nsli_layoutEngine] -> <NSISEngine 0x6000001812b0> ... [<CustomLabel 0x7fcb8350d0d0>/UIView _nsis_compatibleBoundsInEngine:<NSISEngine 0x6000001812b0>] -> {{20, 20}, {25000000, 20.5}} Xtrace!
  26. Разбираемся с UILabel (Поиск API) 35 Бинго! [<CustomLabel 0x7fcb8350d0d0>/UILabel _needsDoubleUpdateConstraintsPass]

    -> 1 ... [<CustomLabel 0x7fcb8350d0d0>/UIView nsli_layoutEngine] -> <NSISEngine 0x6000001812b0> ... [<CustomLabel 0x7fcb8350d0d0>/UIView _nsis_compatibleBoundsInEngine:<NSISEngine 0x6000001812b0>] -> {{20, 20}, {25000000, 20.5}} Xtrace!
  27. Разбираемся с UILabel (Поиск API) 36 Бинго! func _nsis_compatibleBoundsInEngine(_ engine:

    NSISEngine) func nsli_layoutEngine() -> NSISEngine ▌ Теперь ясно как вытащить размер из движка!
  28. NSISEngine Решение системы констрейнтов Обновление ICS констрейнтов Обновление ICS констрейнтов

    Решение системы констрейнтов Нужен второй проход? 37 Разбираемся с UILabel (Поиск API) *ICS = IntrinsicContentSize false true LayoutEngineBounds
  29. 39 @objc func _needsDoubleUpdateConstraintsPass() -> Bool { return true }

    override var intrinsicContentSize: CGSize { return attributedText?.size( forWidth: (engineBounds ?? bounds).width ) ?? .zero } var engineBounds: CGRect? { return ... } ASLabel
  30. 40 @objc func _needsDoubleUpdateConstraintsPass() -> Bool { return true }

    override var intrinsicContentSize: CGSize { return attributedText?.size( forWidth: (engineBounds ?? bounds).width ) ?? .zero } var engineBounds: CGRect? { return ... } ASLabel
  31. 41 @objc func _needsDoubleUpdateConstraintsPass() -> Bool { return true }

    override var intrinsicContentSize: CGSize { return attributedText?.size( forWidth: (engineBounds ?? bounds).width ) ?? .zero } var engineBounds: CGRect? { return ... } ASLabel
  32. 42 var engineBounds: CGRect? { let objcSelector = Selector("_nsis_compatibleBoundsInEngine:") let

    impl = class_getMethodImplementation( type(of: self).self, objcSelector ) typealias CFunction = @convention(c) (AnyObject, Selector, Any) -> CGRect let callableImpl = unsafeBitCast(impl, to: CFunction.self) return layoutEngine.flatMap { callableImpl(self, objcSelector, $0) } } ASLabel
  33. 43 var engineBounds: CGRect? { let objcSelector = Selector("_nsis_compatibleBoundsInEngine:") let

    impl = class_getMethodImplementation( type(of: self).self, objcSelector ) typealias CFunction = @convention(c) (AnyObject, Selector, Any) -> CGRect let callableImpl = unsafeBitCast(impl, to: CFunction.self) return layoutEngine.flatMap { callableImpl(self, objcSelector, $0) } } ASLabel
  34. 44 var engineBounds: CGRect? { let objcSelector = Selector("_nsis_compatibleBoundsInEngine:") let

    impl = class_getMethodImplementation( type(of: self).self, objcSelector ) typealias CFunction = @convention(c) (AnyObject, Selector, Any) -> CGRect let callableImpl = unsafeBitCast(impl, to: CFunction.self) return layoutEngine.flatMap { callableImpl(self, objcSelector, $0) } } ASLabel
  35. 45 var engineBounds: CGRect? { let objcSelector = Selector("_nsis_compatibleBoundsInEngine:") let

    impl = class_getMethodImplementation( type(of: self).self, objcSelector ) typealias CFunction = @convention(c) (AnyObject, Selector, Any) -> CGRect let callableImpl = unsafeBitCast(impl, to: CFunction.self) return layoutEngine.flatMap { callableImpl(self, objcSelector, $0) } } ASLabel
  36. 46 var engineBounds: CGRect? { let objcSelector = Selector("_nsis_compatibleBoundsInEngine:") let

    impl = class_getMethodImplementation( type(of: self).self, objcSelector ) typealias CFunction = @convention(c) (AnyObject, Selector, Any) -> CGRect let callableImpl = unsafeBitCast(impl, to: CFunction.self) return layoutEngine.flatMap { callableImpl(self, objcSelector, $0) } } ASLabel
  37. 47 var layoutEngine: Any? { let objcSelector = Selector("_nsli_layoutEngine") let

    impl = class_getMethodImplementation( type(of: self).self, objcSelector ) typealias CFunction = @convention(c) (AnyObject, Selector) -> Any let callableImpl = unsafeBitCast(impl, to: CFunction.self) return callableImpl(self, objcSelector) } ASLabel
  38. ASLabel 49 › Не надо переопределять systemLayoutSizeFitting › Не надо

    готовить все слои view-tree под ASLabel › Никакой дополнительной работы › Повторяет поведение UILabel
  39. Результат 50 Первый уровень › Определение intrinsicContentSize › Вызов второго

    layout-pass посредством invalidateIntrinsicContentSize в layoutSubviews