Od Xcode 8 i iOS10 widoki nie są odpowiednio dopasowane do viewDidLayoutSubviews

95

Wygląda na to, że przy włączonym Xcode 8 viewDidLoadwszystkie podglądy kontrolera widoku mają ten sam rozmiar 1000x1000. Dziwna rzecz, ale w porządku, viewDidLoadnigdy nie było lepszym miejscem do poprawnego rozmiaru widoków.

Ale viewDidLayoutSubviewsjest!

A na moim obecnym projekcie staram się wydrukować rozmiar przycisku:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

Dziennik pokazuje rozmiar (1000x1000) dla myButton! Następnie, jeśli loguję się na przycisk, na przykład, dziennik pokazuje normalny rozmiar.

Używam autoukładu.

Czy to błąd?

Jaskółka oknówka
źródło
1
Mając ten sam problem z UIImageView - kiedy drukuję, otrzymuję dziwną ramkę = (0 0; 1000 1000) ;. Jestem wewnątrz UITableViewCell i po odświeżeniu widoku tabeli ramka jest taka, jakiej oczekuję (również gdy komórka wychodzi z widoku i wraca ponownie). Czy ktoś ma pojęcie, dlaczego tak się dzieje (domyślnie dziwna ramka)?
Eugen Dimboiu,
4
Myślę, że (0, 0, 1000, 1000)powiązana inicjalizacja to nowy sposób, w jaki Xcode inicjuje widoki z IB. Przed Xcode8, widoki były tworzone ze skonfigurowanym rozmiarem w xib, a następnie zmieniane zgodnie z ekranem zaraz po. Ale teraz w dokumencie IB nie ma skonfigurowanego rozmiaru, ponieważ rozmiar zależy od wyboru urządzenia (na dole ekranu). Tak więc prawdziwe pytanie brzmi: czy istnieje wiarygodne miejsce, w którym można sprawdzić ostateczną wielkość wyświetleń?
Martin
4
czy używasz zaokrąglonych rogów na swoim przycisku? Spróbuj wcześniej wywołać layoutIfNeeded ().
Eugen Dimboiu,
Ciekawy. Rzeczywiście używałem ramki widoku do obliczenia okrągłej granicy. Nawet jeśli nie odpowiada na pytanie, działa. To dobra wskazówka, o której należy pamiętać. Dzięki!
Martin
Myślę, że mam podobne problemy z konfiguracją przycisku obrazu w prawym widoku uitextfield. Chciałem ustawić wysokość i szerokość przycisku obrazu na wysokość pola tekstowego, aby zachował proporcje i wypadł z kontenera.
atlantach_james

Odpowiedzi:

98

Teraz Interface Builder pozwala użytkownikowi dynamicznie zmieniać rozmiar każdego kontrolera widoku w scenorysie, aby zasymulować rozmiar określonego urządzenia.

Przed tą funkcją użytkownik powinien ręcznie ustawić każdy rozmiar kontrolera widoku. Więc kontroler widoku został zapisany z określonym rozmiarem, który został użyty initWithCoderdo ustawienia początkowej klatki.

Teraz wygląda na to, że initWithCodernie używaj rozmiaru zdefiniowanego w scenorysie i zdefiniuj rozmiar 1000x1000 pikseli dla widoku kontrolera widoku i wszystkich jego podglądów podrzędnych.

Nie stanowi to problemu, ponieważ widoki powinny zawsze używać jednego z następujących rozwiązań układu:

  • autolayout, a wszystkie ograniczenia będą poprawnie układać widoki

  • autoresizingMask, która będzie układać każdy widok, do którego nie są dołączone żadne ograniczenia ( uwaga: automatyczne układanie i ograniczenia marginesów są teraz zgodne w tym samym widoku \ o /! )

Ale to jest problem dla wszystkich rzeczy układu związanego z warstwą widoku, jak cornerRadius, ponieważ ani autolayout ani automatycznej zmiany maska dotyczy właściwości warstw.

Aby odpowiedzieć na ten problem, typowym sposobem jest użycie, viewDidLayoutSubviewsjeśli jesteś w kontrolerze lub layoutSubviewjeśli jesteś w widoku. W tym momencie (nie zapomnij wywołać ich superwzględnych metod), jesteś prawie pewien, że wszystko zostało zrobione!

Całkiem pewny? Hum ... nie do końca, zauważyłem, i dlatego zadałem to pytanie, w niektórych przypadkach widok nadal ma rozmiar 1000x1000 w tej metodzie. Myślę, że nie ma odpowiedzi na moje własne pytanie. Aby podać jak najwięcej informacji na ten temat:

1- Dzieje się to tylko podczas układania komórek! W UITableViewCell& UICollectionViewCellpodklasach, layoutSubviewnie będzie wywoływane po prawidłowym rozmieszczeniu podglądów podrzędnych.

2- Jak zauważył @EugenDimboiu (proszę zagłosuj za jego odpowiedź, jeśli jest dla ciebie przydatna), wywołanie [myView layoutIfNeeded]nie- ułożonego podglądu podrzędnego spowoduje prawidłowe ułożenie go w samą porę.

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

3- Moim zdaniem jest to zdecydowanie błąd. Wysłałem to na radar (id 28562874).

PS: Nie jestem Anglikiem, więc nie krępuj się edytować mojego posta, jeśli moja gramatyka powinna zostać poprawiona;)

PS2: Jeśli masz jakieś lepsze rozwiązanie, nie wahaj się pisać kolejnej odpowiedzi. Przesunę zaakceptowaną odpowiedź.

Jaskółka oknówka
źródło
3
thx, ale nie mogłem znaleźć radaru o identyfikatorze 28562874, czy mogę podać adres URL radaru?
Joey
U mnie zadziałało jak urok, także dla warstw widoku!
hlynbech
@Joey, nie wiem, czy mogę uzyskać link do mojego błędu. Nie znalazłem żadnego bezpośredniego adresu URL i wygląda na to, że inni użytkownicy nie widzą moich raportów. Zgodnie z tą odpowiedzią SO stackoverflow.com/a/145223/127493 wydaje się, że najlepszym sposobem na zwiększenie priorytetu błędu jest utworzenie duplikatu.
Martin
To nie jest głupie ze strony Apple. Mam w pełni określony przez kontroler widoku automatycznego układu, a kilka pól tekstowych i UIView mają tę głupią ramkę 0,0,1000,1000. Ale nie wszystko. Jak można uniknąć kontroli jakości? Przypuszczam, że mogę zgłosić kolejny radar, którego nie przeczytają.
ahwulf
3
Chciałbym podziękować Tobie i wszystkim innym za spostrzeżenia i sugestie. Spędziłam cały dzień na wyrywaniu włosów, ponieważ UIStackViewwnętrze UICollectionViewCellnie przywracało prawidłowej wysokości podczas viewDidLayoutSubviews. Zadzwonienie layoutIfNeedednatychmiast rozwiązało problem.
Ruiz
40

Czy Twój przycisk ma zaokrąglone rogi? Zadzwoń layoutIfNeeded()wcześniej.

Eugen Dimboiu
źródło
1
ahah, próbujesz zdobyć więcej powtórzeń? Jak powiedziałem w swoim komentarzu, to nie odpowiada na pytanie. Jednak pomogło mi to, więc zdobyłeś +1 :)
Martin
4
Może to komuś pomóc w przyszłości i łatwiej to zauważyć w porównaniu z komentarzami
Eugen Dimboiu
1
Pomógł mi właśnie teraz!
daidai
@daidai miło to słyszeć!
Eugen Dimboiu,
to działa! ale dlaczego? ale jakie jest właściwe rozwiązanie, aby uzyskać odpowiednią wielkość ramy?
Crashalot
25

Rozwiązanie: Wrap wszystko wewnątrz viewDidLayoutSubviewsw DispatchQueue.main.async.

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
Derek Soike
źródło
1
to działa, nawet jeśli zostanie zastosowane -viewDidLoad... takie dziwne obejście
medvedNick
1
Prawdopodobnie dlatego, że podczas viewDidLayoutSubviews system nie miał czasu na zrobienie tego, co należało zrobić. Wywołanie wysyłki sprawia, że ​​przeskakujesz o jedną pętlę, pozwalając systemowi dokończyć jego słowo. Jeśli mam rację, możesz uzyskać to samo zachowanie przy śnie trwającym kilka milisekund
thibaut noah
ponieważ wszystkie zadania interfejsu użytkownika muszą być wykonywane w ramach głównego wątku, dziękujemy za rozwiązanie!
danywarner
18

Wiem, że to nie było twoje dokładne pytanie, ale natknąłem się na podobny problem, w którym, podobnie jak podczas aktualizacji, niektóre z moich widoków były pomieszane, mimo że w widoku viewDidLayoutSubviews miał prawidłowy rozmiar klatki. Zgodnie z informacjami o wydaniu iOS 10:

„Wysyłanie layoutuIfNeeded do widoku nie powinno przenosić widoku, ale we wcześniejszych wersjach, jeśli widok miał translatesAutoresizingMaskIntoConstraints ustawione na NO i jeśli był pozycjonowany przez ograniczenia, layoutIfNeeded przesunąłby widok tak, aby pasował do silnika układu przed wysłaniem układu do poddrzewa. Te zmiany korygują to zachowanie, a layoutIfNeeded nie ma wpływu na pozycję odbiornika i zwykle jego rozmiar.

Część istniejącego kodu może polegać na tym nieprawidłowym zachowaniu, które zostało już poprawione. Nie ma zmiany zachowania dla plików binarnych połączonych przed iOS 10, ale podczas tworzenia na iOS 10 może być konieczne poprawienie niektórych sytuacji, wysyłając -layoutIfNeeded do superview widoku translatesAutoresizingMaskIntoConstraints, który był poprzednim odbiornikiem, lub też ustawiając go i zmieniając rozmiar przed ( lub później, w zależności od pożądanego zachowania) layoutIfNeeded.

Aplikacje innych firm z niestandardowymi podklasami UIView korzystającymi z automatycznego układu, które zastępują układ. Podglądy i brudny układ na sobie przed wywołaniem super są narażone na wyzwolenie pętli informacji zwrotnej dotyczącej układu podczas przebudowy w systemie iOS 10. Po prawidłowym wysłaniu kolejnych układów wywołań funkcji Subviews, które muszą być przestań w pewnym momencie brudzić układ na sobie (pamiętaj, że to wywołanie zostało pominięte w wersji poprzedzającej iOS 10). "

Zasadniczo nie można wywołać layoutIfNeeded na obiekcie podrzędnym View, jeśli używasz translatesAutoresizingMaskIntoConstraints - teraz wywołanie layoutIfNeeded musi znajdować się w superView i nadal możesz wywołać to w viewDidLayoutSubviews.

HannahCarney
źródło
5

Jeśli ramki nie są poprawne w layoutSubViews (a nie są), możesz wysłać asynchroniczny fragment kodu w głównym wątku. Daje to systemowi trochę czasu na wykonanie układu. Kiedy wysyłany blok jest wykonywany, ramki mają odpowiednie rozmiary.

Emiel
źródło
3

To rozwiązało dla mnie (absurdalnie irytujący) problem:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

Edycja / Uwaga: to jest dla pełnego ekranu ViewController.

Nerdhappy
źródło
Używanie granic mainScreen nie jest dobrym rozwiązaniem, ponieważ w wielu przypadkach viewController nie zajmuje całej powierzchni ekranu. Ponadto powinieneś wywołać [super viewDidLayoutSubviews];tę metodę z powodu wielu rzeczy związanych z autoukładem wykonywanych przez sam widok
Martin
@Martin, co polecasz? Zgoda, nie wydaje się to idealne.
Crashalot
@Crashalot, jak mówię w mojej odpowiedzi, użycie autolayout lub autoresizingMask poprawiłoby układ twoich plików UIView. Ale jeśli musisz wykonać specjalne obliczenia na określonej ramce widoku, odpowiedź Eugena działa: zadzwoń layoutIfNeededdo niej. Mam wrażenie, że to nie jest najlepsze rozwiązanie, ale nadal nie znalazłem lepszego.
Martin
2

Właściwie viewDidLayoutSubviewsteż nie jest to najlepsze miejsce na kadrowanie widoku. O ile zrozumiałem, od teraz jedynym miejscem, w którym należy to zrobić, jest layoutSubviewsmetoda w kodzie rzeczywistego widoku. Żałuję, że nie mam racji, niech ktoś mnie poprawi, jeśli to nieprawda!

alex_roudique
źródło
dzięki za odpowiedź. Dokumentacja firmy Apple viewDidLayoutSubviewsjest dość niejednoznaczna. Drugie zdanie w „dyskusjach” w pewien sposób przeczy temu ostatniemu. developer.apple.com/reference/uikit/uiviewcontroller/…
Martin
0

Zgłosiłem już ten problem do Apple, ten problem istnieje od dawna, kiedy inicjujesz UIViewController z Xib, ale znalazłem całkiem niezłe obejście. Oprócz tego znalazłem ten problem w niektórych przypadkach, gdy layoutIfNeeded na UICollectionView i UITableView, gdy źródło danych nie jest ustawione w początkowym momencie, i potrzebowałem go również zmienić.

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()
        }
    }
}

Wysyłka po rozszerzeniu:

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()
    }
}

Rozszerzenie 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
    }
}
Michał Zaborowski
źródło
0

Mój problem został rozwiązany poprzez zmianę użycia z

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

do

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

A więc od Did do Will

Super dziwne

Jan
źródło
0

Lepsze rozwiązanie dla mnie.

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_)
    }
}

Za pomocą

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

myButton.add(layoutComplement: CircleShapeComplement())
bizhara
źródło
0

Zastąp układ Podwarstwy (warstwy: CALayer) zamiast podglądy układu w podglądzie komórki, aby mieć prawidłowe klatki

Entro
źródło
0

Jeśli musisz zrobić coś w oparciu o ramkę swojego widoku - nadpisz układSubviews i call layout IfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

Miałem problem z viewDidLayoutSubviews zwracającym niewłaściwą ramkę dla mojego widoku, dla którego musiałem dodać gradient. I tylko layoutIfNeeded zrobił właściwą rzecz :)

Vitya Shurapov
źródło
-1

Zgodnie z nową aktualizacją w iOS jest to w rzeczywistości błąd, ale możemy go zmniejszyć, używając -

Jeśli używasz xib z autoukładem w swoim projekcie, musisz po prostu zaktualizować ramkę w ustawieniach autolayout, znajdź obraz do tego.wprowadź opis obrazu tutaj

neha mishra
źródło