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


94

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

但是viewDidLayoutSubviews是!

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

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

日志显示myButton的大小为(1000x1000)!然后,例如,如果我单击按钮,日志将显示正常大小。

我正在使用自动版式。

是虫子吗?


1
与UIImageView有相同的问题-当我打印时,我得到一个奇怪的帧=(0 0; 1000 1000);. 我位于UITableViewCell中,刷新表格视图后,框架即为我期望的框架(当单元格离开视口并再次返回时)。任何人都知道为什么会这样(默认情况下是奇怪的帧)吗?
Eugen Dimboiu

4
我认为(0, 0, 1000, 1000)绑定初始化是Xcode实例化IB视图的新方法。在Xcode8之前,创建的视图具有在xib中配置的大小,然后紧接着根据屏幕调整大小。但是现在,IB文档中没有配置的大小,因为该大小取决于您选择的设备(在屏幕底部)。因此,真正的问题是:是否存在可以检查视图最终尺寸的可靠位置?
马丁

4
您是否为按钮使用圆角?尝试在之前调用layoutIfNeeded()。
Eugen Dimboiu '16

有趣。我确实是在使用视框来计算圆形边框。即使它不能回答问题,它也可以工作。要牢记一个好技巧。谢谢!
马丁

我认为在uitextfield的rightview内设置图像按钮时遇到类似的问题。我想将图像按钮的高度和宽度设置为文本字段的高度,以使其保持宽高比并影响容器。
atlantach_james

Answers:


98

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

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

现在,似乎initWithCoder不使用情节提要中定义的大小,而是为viewcontroller视图及其所有子视图定义了1000x1000 px的大小。

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

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

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

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

要解决此问题,常用的方法是使用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-我认为,这绝对是一个错误。我已将其提交给雷达(编号28562874)。

PS:我不是英语母语人士,因此,如果我的语法应得到纠正,请随时编辑我的帖子;)

PS2:如果您有更好的解决方案,请不要再写下其他答案。我将移动接受的答案。


3
thx,但是我找不到ID为28562874的雷达,我能找到雷达URL吗?
乔伊,

对我来说,就像魅力一样,对视图的层次也是如此!
hlynbech '16

@Joey,我不知道是否可以找到我的错误的链接。我没有找到任何直接URL,其他用户似乎看不到我的报告。根据此SO答案stackoverflow.com/a/145223/127493,提高错误优先级的最佳方法似乎是复制错误。
马丁

对于苹果公司而言,这是愚蠢的。我有一个由自动布局视图控制器完全指定的控件,并且几个文本字段和一个UIView都具有这个愚蠢的0,0,1000,1000框架。但不是所有的。如何逃避质量检查?我想我可以再提交一个他们不会读的雷达。
ahwulf

3
感谢您和其他所有人的见解和建议。我花了一整天的时间拔出头发,因为UIStackViewUICollectionViewCell梳妆期间a的内部没有返回正确的高度viewDidLayoutSubviewslayoutIfNeeded立即致电来解决问题。
鲁伊斯

40

您是否在使用圆角按钮?尝试致电layoutIfNeeded()之前。


1
啊哈,想获得更多代表?正如我在评论中所说,这并不能回答问题。但是,它对我有所帮助,所以您获得了+1 :)
马丁

4
它可能会在将来对某人有所帮助,与评论相比,它更容易发现
Eugen Dimboiu

1
刚才对我有帮助!
daidai

@daidai很高兴听到这个消息!
Eugen Dimboiu '16

这可行!但为什么?还有,什么是获得适当帧尺寸的正确解决方案?
Crashalot's

24

解决方案:将所有内容包装在viewDidLayoutSubviewsDispatchQueue.main.async

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

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

1
即使放进去,它也能正常工作-viewDidLoad...如此奇怪的解决方法
medvedNick

1
可能是因为当viewDidLayoutSubviews时,系统没有时间去做需要做的事情。调用dispatch使您跳出一个runloop,从而使系统能够完成他的任务。如果我是对的,那么您可以在几毫秒的睡眠中得到相同的行为
thibaut noah

因为所有UI任务必须在主线程中执行,所以谢谢您的解决方案!
danywarner

18

我知道这不是您的确切问题,但是我遇到了一个类似的问题,尽管在viewDidLayoutSubviews中具有正确的帧大小,但在更新时我的一些视图还是被弄乱了。根据iOS 10发行说明:

“发送到视图的layoutIfNeeded不会移动该视图,但是在较早的版本中,如果该视图的translatesAutoresizingMaskIntoConstraints设置为NO,并且如果它被约束定位,则layoutIfNeeded会在发送布局之前移动视图以匹配布局引擎这些更改纠正了此问题,并且接收机的位置以及通常其大小不会受到layoutIfNeeded的影响。

某些现有代码可能依赖于此错误行为,该错误行为现在已纠正。iOS 10之前链接的二进制文件没有行为更改,但是在iOS 10上构建时,您可能需要通过将-layoutIfNeeded发送到先前接收者的translatesAutoresizingMaskIntoConstraints视图的超级视图中来纠正某些情况,或者在之前对其进行定位和调整大小(或之后,取决于您想要的行为)layoutIfNeeded。

使用Auto Layout且具有自定义UIView子类的第三方应用在调用super之前会覆盖layoutSubviews和自身上的脏布局,因此在iOS 10上进行重建时,它们有触发布局反馈循环的风险。当正确发送后续的layoutSubviews调用时,他们必须确保停止在自己身上弄脏布局(请注意,此调用在iOS 10之前的版本中已跳过)。”

本质上,如果您使用的是translatesAutoresizingMaskIntoConstraints,则无法在视图的子对象上调用layoutIfNeeded-现在必须在superView上调用layoutIfNeeded,并且仍然可以在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的。


使用mainScreen边界不是一个好的解决方案,因为在许多情况下,viewController不会占用整个屏幕空间。另外,[super viewDidLayoutSubviews];由于视图本身完成了许多自动布局工作,因此您应该调用此方法
Martin

@马丁你有什么建议?同意这似乎并不理想。
Crashalot's

就像我在回答中说的@Crashalot,使用autolayout或autoresizingMask可以正确地布局您的UIViews。但是,如果您必须在特定的视图框架上执行特殊的计算,则Eugen的答案有效:调用layoutIfNeeded它。我觉得这不是最好的解决方案,但是我仍然没有找到更好的解决方案。
马丁

2

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


感谢您的回答。Apple上的文档viewDidLayoutSubviews非常含糊。“讨论”中的第二句话在某种程度上与最后一个相反。developer.apple.com/reference/uikit/uiviewcontroller/…–
Martin

0

我已经向苹果报告了此问题,很久以来,当您从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
    }
}

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

覆盖cellSubview中的layoutSublayers(层:CALayer)而不是layoutSubviews以具有正确的帧


0

如果您需要根据视图的框架进行操作,请覆盖layoutSubviews并调用layoutIfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

我的问题是viewDidLayoutSubviews返回了错误的视图框架,为此我需要添加一个渐变。而且只有layoutIfNeeded做正确的事情:)


-1

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

如果您在项目中使用带有自动布局的xib,则只需在自动布局设置中更新框架,请为此找到图像。在此处输入图片说明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.