Kể từ Xcode 8 và iOS10, các chế độ xem không được định kích thước chính xác trên viewDidLayoutSubviews


94

Có vẻ như với Xcode 8 viewDidLoad, tất cả các chế độ xem phụ của chế độ xem đều có cùng kích thước 1000x1000. Điều kỳ lạ, nhưng không sao,viewDidLoad chưa bao giờ là nơi tốt hơn để kích thước chính xác các lượt xem.

Nhưng viewDidLayoutSubviews là!

Và trong dự án hiện tại của tôi, tôi cố gắng in kích thước của một nút:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

Nhật ký hiển thị kích thước (1000x1000) cho myButton! Sau đó, nếu tôi đăng nhập vào một lần nhấp vào nút, chẳng hạn, nhật ký hiển thị kích thước bình thường.

Tôi đang sử dụng autolayout.

Nó là một lỗi?


1
Gặp vấn đề tương tự với UIImageView - khi tôi in, tôi nhận được một khung hình lạ = (0 0; 1000 1000) ;. Tôi đang ở trong UITableViewCell và khi tôi làm mới chế độ xem bảng, khung hình sẽ như tôi mong đợi (cũng như khi ô đi ra khỏi chế độ xem và quay trở lại). Bất cứ ai có bất kỳ ý tưởng tại sao điều này xảy ra (khung hình kỳ lạ theo mặc định)?
Eugen Dimboiu

4
Tôi nghĩ rằng (0, 0, 1000, 1000)khởi tạo ràng buộc là cách mới để Xcode thể hiện các chế độ xem từ IB. Trước Xcode8, các khung nhìn đã được tạo với kích thước được định cấu hình của chúng trong xib, sau đó được thay đổi kích thước theo màn hình ngay sau đó. Nhưng hiện tại, không có kích thước được định cấu hình trong tài liệu IB vì kích thước phụ thuộc vào lựa chọn thiết bị của bạn (ở cuối màn hình). Vì vậy, câu hỏi thực sự là: có nơi nào đáng tin cậy để kiểm tra kích thước cuối cùng của lượt xem không?
Martin

4
bạn có đang sử dụng các góc tròn cho nút của mình không? Hãy thử gọi layoutIfNeeded () trước.
Eugen Dimboiu

Hấp dẫn. Tôi đã thực sự sử dụng khung xem để tính toán một đường viền tròn. Ngay cả khi nó không trả lời câu hỏi, nó vẫn hoạt động. Đó là một mẹo hay cần ghi nhớ. Cảm ơn!
Martin

Tôi nghĩ rằng tôi đang gặp sự cố tương tự khi thiết lập nút hình ảnh bên trong chế độ xem bên phải của trường uitext. Tôi muốn đặt chiều cao và chiều rộng của nút hình ảnh bằng chiều cao của trường văn bản để nó duy trì tỷ lệ khung hình của nó và thoát ra khỏi vùng chứa.
atlantach_james

Câu trả lời:


98

Giờ đây, Interface Builder cho phép người dùng thay đổi kích thước động của mọi bộ điều khiển chế độ xem trong bảng phân cảnh, để mô phỏng kích thước của một thiết bị nhất định.

Trước chức năng này, người dùng nên đặt thủ công từng kích thước bộ điều khiển chế độ xem. Vì vậy, bộ điều khiển khung nhìn đã được lưu với một kích thước nhất định, được sử dụng initWithCoderđể thiết lập khung hình ban đầu.

Bây giờ, có vẻ như initWithCoderkhông sử dụng kích thước được xác định trong bảng phân cảnh và xác định kích thước 1000x1000 px cho chế độ xem điều khiển và tất cả các chế độ xem phụ của nó.

Đây không phải là vấn đề, bởi vì các khung nhìn luôn phải sử dụng một trong các giải pháp bố cục sau:

  • autolayout và tất cả các ràng buộc sẽ bố trí chính xác chế độ xem của bạn

  • autoresizingMask, sẽ bố trí từng chế độ xem không có bất kỳ ràng buộc nào kèm theo ( lưu ý rằng các ràng buộc tự động thanh toán và lề hiện tương thích trong cùng một chế độ xem \ o /! )

Nhưng đây một vấn đề đối với tất cả nội dung bố cục liên quan đến lớp xem, chẳng hạn như cornerRadius, không tự động thanh toán hay mặt nạ tự động khôi phục áp dụng cho các thuộc tính của lớp.

Để trả lời vấn đề này, cách phổ biến là sử dụng viewDidLayoutSubviewsnếu bạn đang ở trong bộ điều khiển hoặc layoutSubviewnếu bạn đang ở trong một chế độ xem. Tại thời điểm này (đừng quên gọi cho họsuper phương thức tương đối ), bạn khá chắc chắn rằng mọi thứ về bố cục đã được thực hiện!

Khá chắc chắn? Hum ... không hoàn toàn, tôi đã nhận xét, và đó là lý do tại sao tôi hỏi câu hỏi này, trong một số trường hợp, khung nhìn vẫn có kích thước 1000x1000 trên phương pháp này. Tôi nghĩ rằng không có câu trả lời cho câu hỏi của riêng tôi. Để cung cấp thông tin tối đa về nó:

1- nó chỉ xảy ra khi đặt ra các ô! Trong UITableViewCell& UICollectionViewCelllớp con, layoutSubviewsẽ không được gọi sau khi các lượt xem phụ được sắp xếp chính xác.

2- Như @EugenDimboiu đã nhận xét (vui lòng ủng hộ câu trả lời của anh ấy nếu hữu ích cho bạn), việc kêu gọi [myView layoutIfNeeded]chế độ xem phụ không bố cục sẽ bố cục chính xác đúng lúc.

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

3- Theo tôi, đây chắc chắn là một lỗi. Tôi đã gửi nó cho radar (id 28562874).

Tái bút: Tôi không phải là người bản xứ Anh, vì vậy hãy thoải mái chỉnh sửa bài đăng của tôi nếu ngữ pháp của tôi cần được sửa;)

PS2: Nếu bạn có giải pháp nào tốt hơn, đừng viết câu trả lời khác. Tôi sẽ chuyển câu trả lời được chấp nhận.


3
thx, nhưng tôi không thể tìm thấy radar có id 28562874, tôi có thể có URL radar không?
Joey

Làm việc như một sự quyến rũ đối với tôi, cũng cho các lớp của chế độ xem!
hlynbech

@Joey, tôi không biết liệu tôi có thể lấy liên kết về lỗi của tôi không. Tôi không tìm thấy bất kỳ URL trực tiếp nào và có vẻ như những người dùng khác không thể xem báo cáo của tôi. Theo câu trả lời SO này stackoverflow.com/a/145223/127493 , có vẻ như cách tốt nhất để tăng mức độ ưu tiên của lỗi là tạo một bản sao.
Martin

Điều này thật ngu ngốc về phía Apple. Tôi có một bộ điều khiển chế độ xem bố cục tự động được chỉ định đầy đủ và một số trường văn bản và UIView đều có khung 0,0,1000,1000 ngu ngốc này. Nhưng không phải tất cả. Làm thế nào để thoát khỏi QA này? Tôi cho rằng tôi có thể gửi một Radar khác mà họ sẽ không đọc.
ahwulf

3
Tôi muốn cảm ơn bạn và tất cả những người khác vì những hiểu biết và đề xuất của bạn. Tôi đã dành cả ngày để nhổ tóc bởi vì một UIStackViewbên trong của một UICollectionViewCellkhông trở lại chiều cao chính xác trong suốt thời gian đó viewDidLayoutSubviews. Gọi điện layoutIfNeededngay lập tức khắc phục sự cố.
Ruiz

40

Bạn có đang sử dụng các góc tròn cho nút của mình không? Hãy thử gọi layoutIfNeeded()trước.


1
ahah, cố gắng nhận thêm rep? Như tôi đã nói trong bình luận của mình, điều này không trả lời câu hỏi. Tuy nhiên, nó đã giúp tôi nên bạn đã kiếm được +1 :)
Martin

4
Nó có thể giúp một người nào đó trong tương lai, và nó dễ dàng hơn để phát hiện so với bình luận
Eugen Dimboiu

1
Vừa rồi đã giúp tôi!
daidai

@daidai rất vui khi biết điều đó!
Eugen Dimboiu 29/09/16

những công việc này! nhưng tại sao? nhưng giải pháp phù hợp để có được kích thước khung hình thích hợp là gì?
Crashalot 29/09/16

24

Giải pháp: Gói mọi thứ bên trong viewDidLayoutSubviewsvào trong DispatchQueue.main.async.

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}

1
điều này đang hoạt động ngay cả khi được đưa vào -viewDidLoad... một cách giải quyết kỳ lạ như vậy
medvedNick

1
Có thể là vì khi viewDidLayoutSubviews, hệ thống không có thời gian để làm những gì cần phải làm. Gọi công văn khiến bạn nhảy một vòng, cho phép hệ thống kết thúc lời nói của anh ta. Nếu tôi ngay khi bạn có thể nhận được các hành vi tương tự với một giấc ngủ của một vài mili giây
Thibaut noah

bởi vì tất cả các tác vụ giao diện người dùng phải được thực hiện trong chuỗi chính, cảm ơn giải pháp của bạn!
danywarner

18

Tôi biết đây không phải là câu hỏi chính xác của bạn, nhưng tôi đã gặp phải một vấn đề tương tự khi trên bản cập nhật, một số chế độ xem của tôi bị lộn xộn mặc dù có kích thước khung hình chính xác trong viewDidLayoutSubviews. Theo ghi chú phát hành iOS 10:

"Gửi layoutIfNeeded đến một chế độ xem không được mong đợi sẽ di chuyển chế độ xem, nhưng trong các bản phát hành trước đó, nếu chế độ xem đã đặt translateAutoresizingMaskIntoConstraints thành KHÔNG và nếu nó được định vị bởi các ràng buộc, layoutIfNeeded sẽ di chuyển chế độ xem để khớp với công cụ bố cục trước khi gửi bố cục vào cây con. Những thay đổi này sẽ sửa hành vi này và vị trí của người nhận và thường kích thước của nó sẽ không bị ảnh hưởng bởi layoutIfNeeded.

Một số mã hiện có có thể dựa trên hành vi không chính xác này và hiện đã được sửa chữa. Không có thay đổi hành vi nào đối với các tệp nhị phân được liên kết trước iOS 10, nhưng khi xây dựng trên iOS 10, bạn có thể cần phải sửa một số tình huống bằng cách gửi -layoutIfNeeded tới chế độ xem superview của chế độ xem dịchAutoresizingMaskIntoConstraints là máy thu trước đó hoặc định vị và định cỡ nó trước đó ( hoặc sau đó, tùy thuộc vào hành vi mong muốn của bạn) layoutIfNeeded.

Các ứng dụng của bên thứ ba có các lớp con UIView tùy chỉnh sử dụng Bố cục tự động ghi đè lên layoutSubviews và bản thân bố cục bẩn trước khi gọi super có nguy cơ kích hoạt vòng lặp phản hồi bố cục khi chúng xây dựng lại trên iOS 10. Khi chúng được gửi chính xác các lệnh gọi layoutSubviews tiếp theo, họ phải chắc chắn ngừng tự làm bẩn bố cục vào một lúc nào đó (lưu ý rằng lệnh gọi này đã bị bỏ qua trong bản phát hành trước iOS 10). "

Về cơ bản, bạn không thể gọi layoutIfNeeded trên một đối tượng con của View nếu bạn đang sử dụng translateAutoresizingMaskIntoConstraints - bây giờ việc gọi layoutIfNeeded phải ở trên superView và bạn vẫn có thể gọi điều này trong viewDidLayoutSubviews.


5

Nếu các khung không chính xác trong layoutSubViews (mà không phải), bạn có thể gửi một chút mã không đồng bộ trên luồng chính. Điều này cho phép hệ thống có một khoảng thời gian để thực hiện việc bố trí. Khi khối bạn gửi được thực thi, các khung có kích thước phù hợp.


3

Điều này đã khắc phục sự cố (cực kỳ khó chịu) cho tôi:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

Chỉnh sửa / Lưu ý: Đây là dành cho ViewController toàn màn hình.


Sử dụng các giới hạn của mainScreen không phải là một giải pháp tốt vì trong nhiều trường hợp, viewController không chiếm toàn bộ không gian màn hình. Ngoài ra, bạn nên gọi [super viewDidLayoutSubviews];trong phương thức này vì nhiều nội dung tự động thanh toán được thực hiện bởi chính chế độ xem
Martin

@Martin bạn giới thiệu gì? Đồng ý rằng điều này có vẻ không lý tưởng.
Crashalot

@Crashalot như tôi đã nói trong câu trả lời của mình, sử dụng autolayout hoặc autoresizingMask sẽ bố cục chính xác UIViews của bạn . Nhưng nếu bạn phải thực hiện tính toán đặc biệt trên một khung nhìn nhất định, câu trả lời của Eugen sẽ hoạt động: hãy gọi layoutIfNeedednó. Tôi có cảm giác đây không phải là giải pháp tốt nhất, nhưng tôi vẫn chưa tìm ra cách nào tốt hơn.
Martin

2

Trên thực tế viewDidLayoutSubviewscũng không phải là nơi tốt nhất để thiết lập khung nhìn của bạn. Theo như tôi hiểu, từ bây giờ nơi duy nhất nó nên được thực hiện là layoutSubviewsphương thức trong mã của chế độ xem thực tế. Tôi ước gì tôi không đúng, ai đó hãy sửa tôi nếu nó không đúng!


cảm ơn câu trả lời của bạn. Tài liệu của Apple về viewDidLayoutSubviewskhá mơ hồ. Câu thứ hai trong "cuộc thảo luận" mâu thuẫn với câu cuối cùng. developer.apple.com/reference/uikit/uiviewcontroller/…
Martin

0

Tôi đã báo cáo vấn đề này với apple, vấn đề này đã tồn tại từ lâu, khi bạn đang khởi tạo UIViewController từ Xib, nhưng tôi đã tìm thấy cách giải quyết khá tốt. Ngoài ra, tôi đã phát hiện thấy vấn đề đó trong một số trường hợp khi layoutIfNeeded trên UICollectionView và UITableView khi nguồn dữ liệu không được đặt ở thời điểm ban đầu và cũng cần thay đổi nó.

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

Gửi một lần gia hạn:

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

Tiện ích mở rộng 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

Vấn đề của tôi đã được giải quyết bằng cách thay đổi cách sử dụng từ

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

đến

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

Vì vậy, từ Did đến Will

Siêu kỳ lạ


0

Giải pháp tốt hơn cho tôi.

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

Sử dụng

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

myButton.add(layoutComplement: CircleShapeComplement())

0

Ghi đè layoutSublayers (của lớp: CALayer) thay vì layoutSubviews trong ô xem phụ để có khung chính xác


0

Nếu bạn cần làm điều gì đó dựa trên khung của chế độ xem của mình - hãy ghi đè layoutSubviews và gọi layoutIfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

Tôi đã gặp sự cố với việc viewDidLayoutSubviews trả lại khung hình không chính xác cho chế độ xem của tôi, mà tôi cần thêm độ dốc. Và chỉ layoutIfNeeded đã làm đúng :)


-1

Theo bản cập nhật mới trong iOS, đây thực sự là một lỗi nhưng chúng ta có thể giảm điều này bằng cách sử dụng -

Nếu bạn đang sử dụng xib với autolayout trong dự án của mình thì bạn chỉ cần cập nhật frame trong cài đặt autolayout, vui lòng tìm hình ảnh cho điều này.nhập mô tả hình ảnh ở đây

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.