Since Xcode 8 and iOS10, views are not sized properly on viewDidLayoutSubviews

前端 未结 13 1976
北海茫月
北海茫月 2020-11-29 16:48

It seems that with Xcode 8, on viewDidLoad, all viewcontroller subviews have the same size of 1000x1000. Strange thing, but okay, viewDidLoad has n

相关标签:
13条回答
  • 2020-11-29 17:17

    Now, Interface Builder lets the user change dynamically the size of every view controllers in storyboard, to simulate the size of a certain device.

    Before this functionality, the user should set manually each view controller size. So the view controller was saved with a certain size, which was used in initWithCoder to set the initial frame.

    Now, it seems that initWithCoder do not use the size defined in storyboard, and define a 1000x1000 px size for the viewcontroller view & all its subviews.

    This is not a problem, because views should always use either of these layout solutions:

    • autolayout, and all the constraints will layout correctly your views

    • autoresizingMask, which will layout each view which doesn't have any constraint attached to (note autolayout and margin constraints are now compatible in the same view \o/ !)

    But this is a problem for all layout stuff related to the view layer, like cornerRadius, since neither autolayout nor autoresizing mask applies to layer properties.

    To answer this problem, the common way is to use viewDidLayoutSubviews if you are in the controller, or layoutSubview if you are in a view. At this point (don't forget to call their super relative methods), you are pretty sure that all layout stuff has been done!

    Pretty sure? Hum... not totally, I've remarked, and that's why I asked this question, in some cases the view still has its 1000x1000 size on this method. I think there is no answer to my own question. To give the maximum information about it:

    1- it happends only when laying out cells! In UITableViewCell & UICollectionViewCell subclasses, layoutSubview won't be called after subviews would be correctly layed out.

    2- As @EugenDimboiu remarked (please upvote his answer if useful for you), calling [myView layoutIfNeeded] on the not-layed out subview will layout it correctly just in time.

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

    3- To my opinion, this is definitely a bug. I've submitted it to radar (id 28562874).

    PS: I'm not english native, so feel free to edit my post if my grammar should be corrected ;)

    PS2: If you have any better solution, feel free not write another answer. I'll move the accepted answer.

    0 讨论(0)
  • 2020-11-29 17:19

    This fixed the (ridiculously annoying) issue for me:

    - (void) viewDidLayoutSubviews {
    
        [super viewDidLayoutSubviews];
    
        self.view.frame = CGRectMake(0,0,[[UIScreen mainScreen] bounds].size.width,[[UIScreen mainScreen] bounds].size.height);
    
    }
    

    Edit/Note: This is for a full screen ViewController.

    0 讨论(0)
  • 2020-11-29 17:25

    Better solution for me.

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

    Using

    class CircleShapeComplement: LayoutComplementProtocol {
        func didLayoutSubviews(with targetView_: UIView) {
            targetView_.layer.cornerRadius = targetView_.frame.size.height / 2
        }
    }
    
    myButton.add(layoutComplement: CircleShapeComplement())
    
    0 讨论(0)
  • 2020-11-29 17:27

    Actually viewDidLayoutSubviews also is not the best place to set frame of your view. As far as I understood, from now on the only place it should be done is layoutSubviews method in the actual view's code. I wish I wasn't right, someone correct me please if it is not true!

    0 讨论(0)
  • 2020-11-29 17:31

    Override layoutSublayers(of layer: CALayer) instead of layoutSubviews in cell subview to have correct frames

    0 讨论(0)
  • 2020-11-29 17:35

    I already reported this issue to apple, this issue exist since a long time, when you are initializing UIViewController from Xib, but i found quite nice workaround. In addition to that i found that issue in some cases when layoutIfNeeded on UICollectionView and UITableView when datasource is not set in initial moment, and needed also swizzle it.

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

    Dispatch once extension:

    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:

    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 讨论(0)
提交回复
热议问题