it-swarm.com.ru

Начиная с Xcode 8 и iOS10, представления не имеют надлежащего размера на viewDidLayoutSubviews

Похоже, что в Xcode 8 на viewDidLoad все подпредставления viewcontroller имеют одинаковый размер 1000x1000. Странно, но ладно, viewDidLoad никогда не был лучшим местом для правильного определения размера просмотров.

Но viewDidLayoutSubviews есть! 

И в моем текущем проекте я пытаюсь напечатать размер кнопки:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    NSLog(@"%@", self.myButton);
}

Журнал показывает размер (1000x1000) для myButton! Затем, если я, например, нажимаю кнопку, журнал показывает нормальный размер.

Я использую autolayout.

Это ошибка?

87
Martin

Теперь Interface Builder позволяет пользователю динамически изменять размер каждого контроллера представления в раскадровке, чтобы имитировать размер определенного устройства.

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

Теперь кажется, что initWithCoder не использует размер, определенный в раскадровке, и определяет размер 1000x1000 px для представления viewcontroller и всех его подпредставлений.

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

  • autolayout, и все ограничения будут правильно отображать ваши представления

  • autoresizingMask, который будет макетировать каждое представление, к которому не привязаны никакие ограничения (примечание, ограничения autoolayout и margin теперь совместимы в одном представлении\o /!)

Но это является проблемой для всех элементов макета, связанных со слоем вида, например cornerRadius, поскольку ни autolayout, ни autoresizing mask не применяются к свойствам слоя.

Для решения этой проблемы распространенным способом является использование viewDidLayoutSubviews, если вы находитесь в контроллере, или layoutSubview, если вы находитесь в представлении. На этом этапе (не забудьте вызвать их super относительные методы), вы почти уверены, что все элементы макета уже выполнены!

Довольно уверен? Хм ... не полностью, я заметил, и именно поэтому я задал этот вопрос, в некоторых случаях представление по-прежнему имеет размер 1000x1000 для этого метода. Я думаю, что нет ответа на мой собственный вопрос. Чтобы дать максимум информации об этом:

1- это происходит только при размещении клеток! В подклассах UITableViewCell & UICollectionViewCell, layoutSubview не будет вызываться после того, как подпункты будут правильно размещены.

2- Как заметил @EugenDimboiu (пожалуйста, подпишите его ответ, если он полезен для вас), вызов [myView layoutIfNeeded] для невыполненного подпредставления расставит его правильно как раз вовремя.

- (void)layoutSubviews {
    [super layoutSubviews];
    NSLog (self.myLabel); // 1000x1000 size 
    [self.myLabel layoutIfNeeded];
    NSLog (self.myLabel); // normal size
}

3- На мой взгляд, это определенно ошибка. Я отправил его на радар (id 28562874).

PS: я не англичанин, так что не стесняйтесь редактировать мой пост, если моя грамматика должна быть исправлена;)

PS2: Если у вас есть лучшее решение, не стесняйтесь писать другой ответ. Я перенесу принятый ответ.

93
Martin

Вы используете закругленные углы для своей кнопки? Попробуйте позвонить layoutIfNeeded() раньше. 

39
Eugen Dimboiu

Решение: Обернуть все внутри viewDidLayoutSubviews в DispatchQueue.main.async

// Swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
18
Derek Soike

Я знаю, что это не был ваш точный вопрос, но я столкнулся с подобной проблемой, когда во время обновления некоторые мои представления были испорчены, несмотря на наличие правильного размера кадра в viewDidLayoutSubviews. Согласно примечаниям к выпуску iOS 10:

"Отправка layoutIfNeeded в представление не должна перемещать представление, , Но в более ранних выпусках, если представление имело TranslatesAutoresizingMaskIntoConstraints, установленный в NO, и если это было , Будучи позиционированным ограничениями, layoutIfNeeded будет перемещать view.... сопоставить механизм компоновки перед отправкой макета на поддерево. Эти изменения исправляют это поведение, и расположение приемника и обычно его размер не будет зависеть от layoutIfNeeded.

Некоторый существующий код может полагаться на это неправильное поведение сейчас исправлено. Нет никаких изменений поведения для бинарных файлов, связанных ранее iOS 10, но при сборке на iOS 10 может потребоваться исправить некоторые ситуации, отправив -layoutIfNeeded в суперпредставление Представление translationsAutoresizingMaskIntoConstraints, которое было предыдущим приемник, или же позиционирование и определение размера до (или после в зависимости от вашего желаемого поведения) layoutIfNeeded.

Сторонние приложения с пользовательскими подклассами UIView, использующие Auto Layout, которые переопределить layoutSubviews и грязный макет на себя перед вызовом super находятся под угрозой запуска цикла обратной связи макета, когда они перестраиваются на iOS 10. При правильной отправке последующего макета звонки Subviews они должны быть уверены, что в какой-то момент прекратят загрязнять макет самостоятельно (обратите внимание , что этот вызов был пропущен в выпуске до iOS 10). "

По сути, вы не можете вызывать layoutIfNeeded для дочернего объекта View, если вы используете translatesAutoresizingMaskIntoConstraints - теперь вызов layoutIfNeeded должен быть в superView, и вы все равно можете вызвать его в viewDidLayoutSubviews.

18
Hannah Louisa Carney

Это исправило (нелепо раздражающую) проблему для меня:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

    self.view.frame = CGRectMake(0,0,[[UIScreen mainScreen] bounds].size.width,[[UIScreen mainScreen] bounds].size.height);

}

Редактировать/Примечание: это для полноэкранного ViewController.

3
Nerdhappy

Если кадры в layoutSubViews некорректны (что они не являются), вы можете отправить асинхронный фрагмент кода в основном потоке. Это дает системе некоторое время, чтобы сделать макет. Когда блок, который вы отправляете, выполняется, кадры имеют правильные размеры.

3
Emiel

На самом деле viewDidLayoutSubviews также не лучшее место, чтобы установить рамки вашего взгляда. Насколько я понял, теперь единственное место, где это должно быть сделано, - это метод layoutSubviews в коде реального представления. Хотел бы я быть не прав, кто-то поправит меня, пожалуйста, если это неправда!

2
alex_roudique

Моя проблема была решена путем изменения использования с

-(void)viewDidLayoutSubviews{
    [super viewDidLayoutSubviews];
    self.viewLoginMailTop.constant = -self.viewLoginMail.bounds.size.height;
}

в 

-(void)viewWillLayoutSubviews{
    [super viewWillLayoutSubviews];
    self.viewLoginMailTop.constant = -self.viewLoginMail.bounds.size.height;
}

Итак, от ли до воли

Супер странно

0
Jan

Я уже сообщал об этой проблеме в Apple, эта проблема существует с давних времен, когда вы инициализируете UIViewController из Xib, но я нашел довольно хороший обходной путь. В дополнение к этому, я обнаружил эту проблему в некоторых случаях, когда layoutIfNeeded в UICollectionView и UITableView, когда источник данных не установлен в начальный момент, и необходимо также просмотреть его.

extension UIViewController {
    open override class func initialize() {
        if self !== UIViewController.self {
            return
        }
        DispatchQueue.once(token: "io.inspace.uiviewcontroller.swizzle") {
            ins_applyFixToViewFrameWhenLoadingFromNib()
        }
    }

    @objc func ins_setView(view: UIView!) {
        // View is loaded from xib file
        if nibBundle != nil && storyboard == nil && !view.frame.equalTo(UIScreen.main.bounds) {
            view.frame = UIScreen.main.bounds
            view.layoutIfNeeded()
        }
        ins_setView(view: view)
    }

    private class func ins_applyFixToViewFrameWhenLoadingFromNib() {
        UIViewController.swizzle(originalSelector: #selector(setter: UIViewController.view),
                                 with: #selector(UIViewController.ins_setView(view:)))
        UICollectionView.swizzle(originalSelector: #selector(UICollectionView.layoutSubviews),
                                 with: #selector(UICollectionView.ins_layoutSubviews))
        UITableView.swizzle(originalSelector: #selector(UITableView.layoutSubviews),
                                 with: #selector(UITableView.ins_layoutSubviews))
     }
}

extension UITableView {
    @objc fileprivate func ins_layoutSubviews() {
        if dataSource == nil {
            super.layoutSubviews()
        } else {
            ins_layoutSubviews()
        }
    }
}

extension UICollectionView {
    @objc fileprivate func ins_layoutSubviews() {
        if dataSource == nil {
            super.layoutSubviews()
        } else {
            ins_layoutSubviews()
        }
    }
}

Отправка после продления:

extension DispatchQueue {

    private static var _onceTracker = [String]()

    /**
     Executes a block of code, associated with a unique token, only once.  The code is thread safe and will
     only execute the code once even in the presence of multithreaded calls.

     - parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID
     - parameter block: Block to execute once
     */
    public class func once(token: String, block: (Void) -> Void) {
        objc_sync_enter(self); defer { objc_sync_exit(self) }

        if _onceTracker.contains(token) {
            return
        }

        _onceTracker.append(token)
        block()
    }
}

Расширение Swizzle:

extension NSObject {
    @discardableResult
    class func swizzle(originalSelector: Selector, with selector: Selector) -> Bool {

        var originalMethod: Method?
        var swizzledMethod: Method?

        originalMethod = class_getInstanceMethod(self, originalSelector)
        swizzledMethod = class_getInstanceMethod(self, selector)

        if originalMethod != nil && swizzledMethod != nil {
            method_exchangeImplementations(originalMethod!, swizzledMethod!)
            return true
        }
        return false
    }
}
0
mientus

Переопределите layoutSublayers (layer: CALayer) вместо layoutSubviews в подпредставлении ячейки, чтобы иметь правильные кадры

0
Entro

Лучшее решение для меня.

protocol LayoutComplementProtocol {
    func didLayoutSubviews(with targetView_: UIView)
}

private class LayoutCaptureView: UIView {
    var targetView: UIView!
    var layoutComplements: [LayoutComplementProtocol] = []

    override func layoutSubviews() {
        super.layoutSubviews()

        for layoutComplement in self.layoutComplements {
            layoutComplement.didLayoutSubviews(with: self.targetView)
        }
    }
}

extension UIView {
    func add(layoutComplement layoutComplement_: LayoutComplementProtocol) {
        func findLayoutCapture() -> LayoutCaptureView {
            for subView in self.subviews {
                if subView is LayoutCaptureView {
                    return subView as? LayoutCaptureView
                }
            }
            let layoutCapture = LayoutCaptureView(frame: CGRect(x: -100, y: -100, width: 10, height: 10)) // not want to show, want to have size
            layoutCapture.targetView = self
            self.addSubview(layoutCapture)
            return layoutCapture
        }

        let layoutCapture = findLayoutCapture()
        layoutCapture.layoutComplements.append(layoutComplement_)
    }
}

С помощью

class CircleShapeComplement: LayoutComplementProtocol {
    func didLayoutSubviews(with targetView_: UIView) {
        targetView_.layer.cornerRadius = targetView_.frame.size.height / 2
    }
}

myButton.add(layoutComplement: CircleShapeComplement())
0
bizhara