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

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

Avatar for CocoaHeads CocoaHeads
April 26, 2018
370

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

Avatar for CocoaHeads

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