由于 Xcode 8 和 iOS10,viewDidLayoutSubviews 上的视图大小不正确

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

似乎 Xcode 8,在 viewDidLoad 上,所有 viewcontroller 子视图都具有相同的 1000x1000 大小。奇怪的是,但是没关系,viewDidLoad 从来都不是正确调整视图大小的好地方。

但是viewDidLayoutSubviews是!

在我当前的项目中,我尝试打印按钮的大小:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

日志显示 myButton 的大小为 (1000x1000)!然后,如果我登录一个按钮,例如,日志显示正常大小。

我正在使用自动布局。

这是一个错误吗?

实际上 viewDidLayoutSubviews 也不是设置视图框架的最佳位置。据我所知,从现在开始,唯一应该做的地方是实际视图代码中的 layoutSubviews 方法。我希望我说的不对,如果不正确,请有人纠正我!

您的按钮是否使用了圆角? 之前尝试调用 layoutIfNeeded()

我知道这不是你的确切问题,但我 运行 遇到了一个类似的问题,在更新时,尽管 viewDidLayoutSubviews 中的框架大小正确,但我的一些视图还是乱七八糟的。根据 iOS 10 发行说明:

"Sending layoutIfNeeded to a view is not expected to move the view, but in earlier releases, if the view had translatesAutoresizingMaskIntoConstraints set to NO, and if it was being positioned by constraints, layoutIfNeeded would move the view to match the layout engine before sending layout to the subtree. These changes correct this behavior, and the receiver’s position and usually its size won’t be affected by layoutIfNeeded.

Some existing code may be relying on this incorrect behavior that is now corrected. There is no behavior change for binaries linked before iOS 10, but when building on iOS 10 you may need to correct some situations by sending -layoutIfNeeded to a superview of the translatesAutoresizingMaskIntoConstraints view that was the previous receiver, or else positioning and sizing it before (or after, depending on your desired behavior) layoutIfNeeded.

Third party apps with custom UIView subclasses using Auto Layout that override layoutSubviews and dirty layout on self before calling super are at risk of triggering a layout feedback loop when they rebuild on iOS 10. When they are correctly sent subsequent layoutSubviews calls they must be sure to stop dirtying layout on self at some point (note that this call was skipped in release prior to iOS 10)."

本质上,如果您使用 t运行slatesAutoresizingMaskIntoConstraints,则不能在视图的子对象上调用 layoutIfNeeded - 现在必须在 superView 上调用 layoutIfNeeded,您仍然可以调用它在 viewDidLayoutSubviews.

这解决了我的(非常烦人的)问题:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

Edit/Note:这是全屏ViewController。

现在,Interface Builder 允许用户动态更改故事板中每个视图控制器的大小,以模拟特定设备的大小。

在此功能之前,用户应该手动设置每个视图控制器的大小。所以view controller被保存了一定的大小,用于initWithCoder设置初始帧

现在,initWithCoder 似乎没有使用故事板中定义的大小,而是为 viewcontroller 视图及其所有子视图定义了 1000x1000 像素的大小。

这不是问题,因为视图应始终使用以下任一布局解决方案:

  • 自动布局,所有约束都会正确布局您的视图

  • autoresizingMask,它将布局每个没有任何约束的视图(注意自动布局和边距约束现在在同一视图中兼容 \o/ !)

但这所有与视图层相关的布局问题,例如cornerRadius,因为autolayout 自动调整蒙版也不适用于图层属性。

要回答这个问题,通常的方法是在控制器中使用viewDidLayoutSubviews,或者在视图中使用layoutSubview。此时(不要忘记调用它们的 super 相关方法),您可以确定所有布局都已完成!

漂亮确定吗?嗯......不完全是,我说过,这就是我问这个问题的原因,在某些情况下,视图在这种方法上仍然有 1000x1000 的大小。我认为我自己的问题没有答案。提供有关它的最大信息:

1-只有在布局单元格时才会发生!在 UITableViewCellUICollectionViewCell 子类中,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:我不是英语母语,所以如果我的语法需要更正,请随时编辑我的 post ;)

PS2:如果你有更好的解决办法,欢迎不另写答案。我将移动已接受的答案。

如果 layoutSubViews 中的帧不正确(它们不正确),您可以在主线程上分派一些异步代码。这给了系统一些时间来进行布局。当您分派的块被执行时,框架具有适当的大小。

我已经向苹果报告了这个问题,这个问题已经存在很长时间了,当你从 Xib 初始化 UIViewController 时,但我发现了一个很好的解决方法。除此之外,我发现在某些情况下,当初始时刻未设置数据源时,UICollectionView 和 UITableView 上的 layoutIfNeeded 会出现该问题,并且还需要调整它。

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

我的问题已通过更改

的用法得到解决
-(void)viewDidLayoutSubviews{
    [super viewDidLayoutSubviews];
    self.viewLoginMailTop.constant = -self.viewLoginMail.bounds.size.height;
}

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

所以,从做过到愿意

超级奇怪

对我来说更好的解决方案。

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

根据 ios 中的新更新,这实际上是一个错误,但我们可以通过使用 -

来减少它

如果您在项目中使用带自动布局的 xib,那么您只需 更新自动布局设置中的框架 请为此找到图像。

在单元格子视图中覆盖 layoutSublayers(层:CALayer)而不是 layoutSubviews 以获得正确的框架

解决方法: 把所有东西都放在viewDidLayoutSubviews里面,放在DispatchQueue.main.async里面。

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

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

如果您需要根据视图的框架执行某些操作 - 覆盖 layoutSubviews 并调用 layoutIfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

我遇到了 viewDidLayoutSubviews 为我的视图返回错误帧 的问题,我需要为此添加渐变。只有 layoutIfNeeded 做对了:)