web-dev-qa-db-ja.com

Xcode 8およびiOS10以降、viewDidLayoutSubviewsでビューのサイズが適切に設定されていません

Xcode 8では、viewDidLoadで、すべてのviewcontrollerサブビューのサイズが同じ1000x1000であるようです。奇妙なことですが、大丈夫、viewDidLoadはビューのサイズを正しく設定するのに最適な場所ではありませんでした。

しかし、viewDidLayoutSubviewsは!

そして現在のプロジェクトでは、ボタンのサイズを印刷しようとしています:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

ログには、myButtonのサイズ(1000x1000)が表示されます!次に、たとえばボタンをクリックしてログオンすると、ログには通常のサイズが表示されます。

自動レイアウトを使用しています。

バグですか?

90
Martin

Interface Builderでは、ユーザーがストーリーボード内のすべてのView Controllerのサイズを動的に変更して、特定のデバイスのサイズをシミュレートできるようになりました。

この機能の前に、ユーザーは各View Controllerのサイズを手動で設定する必要があります。そのため、View Controllerは特定のサイズで保存され、initWithCodername__で初期フレームを設定するために使用されました。

現在、initWithCodername__はストーリーボードで定義されたサイズを使用せず、ViewControllerビューとそのすべてのサブビューに1000x1000ピクセルのサイズを定義しているようです。

ビューは常に次のいずれかのレイアウトソリューションを使用する必要があるため、これは問題ではありません。

  • 自動レイアウト、およびすべての制約がビューを正しくレイアウトします

  • autoresizingMaskは、制約が付加されていない各ビューをレイアウトします(自動レイアウトとマージンの制約は、同じビューで互換性があることに注意してください\ o /!

しかし、このisは、cornerRadiusname__など、ビューレイヤーに関連するすべてのレイアウトに関する問題です。これはautolayoutautoresizing maskもレイヤープロパティに適用されないためです。

この問題に答えるための一般的な方法は、コントローラーを使用している場合はviewDidLayoutSubviewsname__を使用し、ビューを使用している場合はlayoutSubviewname__を使用することです。この時点で(supername__相対メソッドを呼び出すことを忘れないでください)、すべてのレイアウトが完了していることを確認できます!

きれい確か?ええと...完全にではありませんが、私はこの質問をしたのはそのためです。場合によっては、この方法ではビューのサイズが1000x1000のままであることがあります。私自身の質問に対する答えはないと思います。最大限の情報を提供するには:

1-セルのレイアウト時にのみ発生します! UITableViewCellname__およびUICollectionViewCellname__サブクラスでは、layoutSubviewname__は呼び出されませんafterサブビューは正しくレイアウトされます。

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:英語のネイティブではないので、文法を修正する必要がある場合は投稿を自由に編集してください;)

PS2:より良い解決策があれば、別の答えを書かないでください。受け入れられた答えを移動します。

96
Martin

ボタンに丸い角を使用していますか?前にlayoutIfNeeded()を呼び出してみてください。

39
Eugen Dimboiu

解決策:DispatchQueue.main.asyncviewDidLayoutSubviews内のすべてをラップします。

// Swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
18
Derek Soike

私はこれがあなたの正確な質問ではなかったことを知っていますが、更新時にviewDidLayoutSubviewsに正しいフレームサイズがあるにもかかわらず、私のビューのいくつかが台無しになるという同様の問題に遭遇しました。 iOS 10リリースノートによると:

「ビューにlayoutIfNeededを送信するとビューが移動することは想定されていませんが、以前のリリースでは、ビューのNOに設定されたAutoresizingMaskIntoConstraintsがあり、制約によって配置されていた場合、layoutIfNeededはレイアウトを送信する前にレイアウトエンジンに一致するようにビューを移動しましたこれらの変更はこの動作を修正し、レシーバーの位置とそのサイズは通常、layoutIfNeededの影響を受けません。

一部の既存のコードは、現在修正されているこの不正な動作に依存している可能性があります。 iOS 10より前にリンクされたバイナリの動作に変更はありませんが、iOS 10でビルドする場合、-layoutIfNeededを以前のレシーバーであったtranslatesAutoresizingMaskIntoConstraintsビューのスーパービューに送信するか、それ以外の場合はそれを前に配置およびサイズ変更することにより、いくつかの状況を修正する必要があります(または、希望する動作に応じて)layoutIfNeeded。

Superを呼び出す前にselfでlayoutSubviewsおよびダーティレイアウトをオーバーライドする自動レイアウトを使用するカスタムUIViewサブクラスを持つサードパーティアプリは、iOS 10で再構築するときにレイアウトフィードバックループをトリガーするリスクがあります。ある時点で自分自身のレイアウトを汚すのをやめます(iOS 10より前のリリースではこの呼び出しはスキップされたことに注意してください)。

translatesAutoresizingMaskIntoConstraintsを使用している場合、基本的にViewの子オブジェクトでlayoutIfNeededを呼び出すことはできません-layoutIfNeededの呼び出しはsuperViewで行う必要があり、それでもviewDidLayoutSubviewsで呼び出すことができます。

18

LayoutSubViewsでフレームが正しくない場合(正しくない場合)、メインスレッドで非同期コードをディスパッチできます。これにより、システムにレイアウトを実行する時間が与えられます。ディスパッチしたブロックが実行されると、フレームは適切なサイズになります。

4
Emiel

これは私にとって(途方もなく面倒な)問題を修正しました:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

編集/注:これはフルスクリーンのViewController用です。

3
Nerdhappy

実際、viewDidLayoutSubviewsもビューのフレームを設定するのに最適な場所ではありません。私の知る限り、これから行うべき唯一の場所は、実際のビューのコードのlayoutSubviewsメソッドです。私が正しくなかったなら、誰かが私を修正してください。

2
alex_roudique

私の問題は、使用法を

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

私はすでにこの問題をアップルに報告しました。この問題は、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()
        }
    }
}

ディスパッチ1回拡張:

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
mientus

セルサブビューのlayoutSubviewsではなく、layoutSublayers(layer:CALayer)をオーバーライドして、正しいフレームを使用します。

0
Entro

私にとってより良い解決策。

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
bizhara