Начиная с Xcode 8 и iOS10 размеры представлений в viewDidLayoutSubviews неверны.

95

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

Но viewDidLayoutSubviewsесть!

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

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

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

Я использую автопрокладку.

Это ошибка?

Мартин
источник
1
Такая же проблема с UIImageView - когда я печатаю, я получаю странный кадр = (0 0; 1000 1000) ;. Я нахожусь внутри UITableViewCell, и как только я обновляю представление таблицы, фрейм становится тем, чем я ожидал (также когда ячейка выходит из области просмотра и возвращается снова). Кто-нибудь знает, почему это происходит (странный кадр по умолчанию)?
Eugen Dimboiu
4
Я думаю, что (0, 0, 1000, 1000)связанная инициализация - это новый способ, которым Xcode создает представления из IB. До Xcode8 представления создавались с настроенным размером в xib, а затем изменялись в соответствии с размером экрана сразу после этого. Но теперь в документе IB нет настроенного размера, так как размер зависит от вашего выбора устройства (внизу экрана). Итак, реальный вопрос: есть ли надежное место, где можно было бы проверить окончательный размер просмотров?
Мартин
4
вы используете закругленные углы для кнопки? Попробуйте вызвать layoutIfNeeded () раньше.
Eugen Dimboiu
Интересно. Я действительно использовал рамку обзора для вычисления круглой границы. Даже если он не отвечает на вопрос, он работает. Это хороший совет, о котором следует помнить. Благодарность!
Мартин
Я думаю, что у меня похожие проблемы с настройкой кнопки изображения внутри правого поля uitextfield. Я хотел установить высоту и ширину кнопки изображения на высоту текстового поля, чтобы оно сохраняло свое соотношение сторон и выпадало из контейнера.
atlantach_james

Ответы:

98

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

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

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

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

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

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

Но это является проблемой для всего макета вещи , связанные с видом слоя, как cornerRadius, так как ни autolayout , ни автоматического изменения маски относится к свойствам слоя.

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

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

1- бывает только при раскладке ячеек! В подклассах UITableViewCell& не будет вызываться после правильного размещения вложенных представлений.UICollectionViewCelllayoutSubview

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: Если у вас есть лучшее решение, не стесняйтесь писать другой ответ. Я перенесу принятый ответ.

Мартин
источник
3
спасибо, но я не смог найти радар с идентификатором 28562874, можно мне URL радара?
Joey
Сработало как шарм для меня, в том числе и для слоев обзора!
hlynbech
@Joey, я не знаю, смогу ли я получить ссылку на мою ошибку. Я не нашел прямого URL-адреса, и кажется, что другие пользователи не могут видеть мои отчеты. Согласно этому SO-ответу stackoverflow.com/a/145223/127493 , кажется, что лучший способ повысить приоритет ошибки - создать дубликат.
Мартин
Со стороны Apple это уже не глупо. У меня есть полностью определенный автоматический контроллер представления макета, и несколько текстовых полей и UIView имеют этот дурацкий фрейм 0,0,1000,1000. Но не все. Как это избежать QA? Полагаю, я могу подать еще один радар, который они не прочитают.
ahwulf
3
Я хотел бы поблагодарить вас и всех остальных за ваши идеи и предложения. Я весь день выдергивал волосы, потому что UIStackViewвнутренняя часть UICollectionViewCellне возвращала правильную высоту во время viewDidLayoutSubviews. Звонок layoutIfNeededсразу устранил проблему.
Руис
40

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

Eugen Dimboiu
источник
1
ахах, пытаешься набрать больше репутации? Как я сказал в своем комментарии, это не отвечает на вопрос. Однако мне это помогло, и вы получили +1 :)
Мартин
4
Это может помочь кому-то в будущем, и это легче заметить по сравнению с комментариями,
Евгений Димбою
1
Помогли мне только сейчас!
daidai
@daidai рад это слышать!
Eugen Dimboiu
это работает! но почему? также, но как правильно выбрать размер кадра?
Crashalot
25

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

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
Дерек Сойке
источник
1
это работает, даже если ввести -viewDidLoad... такой странный обходной путь
medvedNick 07
1
Вероятно, потому что при viewDidLayoutSubviews у системы не было времени сделать то, что нужно было сделать. Вызов диспетчеризации заставляет вас перепрыгнуть на один цикл выполнения, позволяя системе закончить свое слово. Если я прав, когда вы могли бы добиться того же поведения,
поспав
потому что все задачи пользовательского интерфейса должны выполняться в основном потоке, спасибо за ваше решение!
danywarner
18

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

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

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

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

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

Ханна Карни
источник
5

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

Эмиель
источник
3

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

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

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

Nerdhappy
источник
Использование границ mainScreen не является хорошим решением, потому что во многих случаях viewController не занимает все пространство экрана. Кроме того, вы должны вызвать [super viewDidLayoutSubviews];этот метод из-за того, что многие функции автоматического раскладки выполняются самим представлением
Мартин
@ Мартин, что ты посоветуешь? Согласен, это не кажется идеальным.
Crashalot
@Crashalot, как я говорю в своем ответе, использование autolayout или autoresizingMask правильно разместит ваш UIViews. Но если вам необходимо выполнить специальные вычисления для определенного кадра просмотра, ответ Евгения работает: вызовите layoutIfNeededего. У меня такое ощущение, что это не лучшее решение, но лучшего пока не нашел.
Мартин
2

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

alex_roudique
источник
спасибо за Ваш ответ. Документация Apple viewDidLayoutSubviewsдовольно неоднозначна. Второе предложение в «дискуссиях» в чем-то противоречит последнему. developer.apple.com/reference/uikit/uiviewcontroller/…
Мартин,
0

Я уже сообщал об этой проблеме в 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

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

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

к

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

Итак, от Did к Will

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

Янв
источник
0

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

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

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

Entro
источник
0

Если вам нужно что-то сделать на основе кадра вашего представления - переопределите layoutSubviews и вызовите layoutIfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

У меня была проблема с viewDidLayoutSubviews, возвращавшим неправильный кадр для моего представления, для которого мне нужно было добавить градиент. И только layoutIfNeeded правильно сделал :)

Витя Шурапов
источник
-1

Согласно новому обновлению в ios, это на самом деле ошибка, но мы можем уменьшить ее, используя -

Если вы используете xib с автоматическим размещением в своем проекте, вам нужно просто обновить фрейм в настройках автоматического размещения, пожалуйста, найдите изображение для этого.введите описание изображения здесь

неха мишра
источник