อะไรคือความสัมพันธ์ระหว่าง setNeedsLayout ของ UIView, layoutIfNeeded และ layoutSubviews?


105

ใครสามารถให้คำอธิบายที่ชัดเจนเกี่ยวกับความสัมพันธ์ระหว่างUIView's setNeedsLayout, layoutIfNeededและlayoutSubviewsวิธีการ? และตัวอย่างการใช้งานที่จะใช้ทั้งสามอย่าง ขอบคุณ.

สิ่งที่ทำให้ฉันสับสนคือถ้าฉันส่งsetNeedsLayoutข้อความดูแบบกำหนดเองของฉันสิ่งต่อไปที่จะเรียกใช้หลังจากวิธีนี้คือการlayoutSubviewsข้ามไปlayoutIfNeededเลย จากเอกสารฉันคาดว่าโฟลว์จะsetNeedsLayout> ทำให้layoutIfNeededถูกเรียก> สาเหตุlayoutSubviewsที่จะเรียก

คำตอบ:


104

ฉันยังคงพยายามคิดออกด้วยตัวเองดังนั้นโปรดใช้สิ่งนี้ด้วยความสงสัยและให้อภัยฉันหากมีข้อผิดพลาด

setNeedsLayoutเป็นเรื่องง่าย: เพียงตั้งค่าสถานะไว้ที่ใดที่หนึ่งใน UIView ซึ่งทำเครื่องหมายว่าเป็นเค้าโครงที่ต้องการ ซึ่งจะบังคับlayoutSubviewsให้เรียกดูก่อนที่จะมีการวาดใหม่ครั้งต่อไป โปรดทราบว่าในหลาย ๆ กรณีคุณไม่จำเป็นต้องเรียกสิ่งนี้อย่างชัดเจนเนื่องจากautoresizesSubviewsคุณสมบัติ หากมีการตั้งค่าไว้ (ซึ่งเป็นค่าเริ่มต้น) การเปลี่ยนแปลงใด ๆ กับเฟรมของมุมมองจะทำให้มุมมองจัดวางมุมมองย่อยของมุมมอง

layoutSubviewsเป็นวิธีการที่คุณทำสิ่งที่น่าสนใจทั้งหมด มันเทียบเท่าdrawRectกับเลย์เอาต์ถ้าคุณต้องการ ตัวอย่างเล็กน้อยอาจเป็น:

-(void)layoutSubviews {
    // Child's frame is always equal to our bounds inset by 8px
    self.subview1.frame = CGRectInset(self.bounds, 8.0, 8.0);
    // It seems likely that this is incorrect:
    // [self.subview1 layoutSubviews];
    // ... and this is correct:
    [self.subview1 setNeedsLayout];
    // but I don't claim to know definitively.
}

layoutIfNeededโดยทั่วไปแล้วAFAIK ไม่ได้หมายถึงการถูกแทนที่ในคลาสย่อยของคุณ มันเป็นวิธีการที่คุณกำลังหมายถึงการเรียกร้องเมื่อคุณต้องการมุมมองที่จะออกมาวางในขณะนี้ การใช้งานของ Apple อาจมีลักษณะดังนี้:

-(void)layoutIfNeeded {
    if (self._needsLayout) {
        UIView *sv = self.superview;
        if (sv._needsLayout) {
            [sv layoutIfNeeded];
        } else {
            [self layoutSubviews];
        }
    }
}

คุณจะเรียกร้องlayoutIfNeededให้มีการกำหนดมุมมอง (และซูเปอร์วิวตามความจำเป็น) ทันที


2
ขอบคุณ - ดีใจที่มีคนตอบคำถามนี้ในที่สุด ในระหว่างนี้ฉันยังต้องเจาะลึก setNeedsDisplay ซึ่งทำให้ drawRect ถูกเรียก - และ contentMode เฉพาะ contentMode = UIViewContentModeRedraw เท่านั้นที่จะทำให้ setNeedsDisplay ถูกเรียกเมื่อขอบเขตของมุมมองเปลี่ยนไป ตัวเลือก contentMode อื่น ๆ จะทำให้มุมมองถูกปรับขนาดเปลี่ยนตามจำนวนหรือจัดชิดขอบเท่านั้น UITableViewCell ที่กำหนดเองของฉันไม่ต้องการวาดใหม่ในการเปลี่ยนแปลงการวางแนว แต่จะปรับขนาดเท่านั้นจนกว่าฉันจะตั้งค่า contentMode เป็น UIViewContentModeRedraw
Tarfa

ฉันคิดว่าบางที [self.subview1 layoutSubviews] ในโค้ดของคุณควรถูกแทนที่ด้วย [self.subview1 setNeedsLayout] เนื่องจาก layoutSubviews ถูกกำหนดให้ถูกแทนที่ แต่ไม่ได้หมายถึงการเรียกจากหรือไปยังมุมมองอื่น กรอบงานจะกำหนดเวลาที่จะเรียกใช้ (โดยการจัดกลุ่มคำขอเค้าโครงหลายรายการเข้าด้วยกันในการเรียกร้องให้มีประสิทธิภาพเพียงครั้งเดียว) หรือคุณทำทางอ้อมโดยการเรียก layoutIfNeeded ที่ใดที่หนึ่งในลำดับชั้นของมุมมอง
Tarfa

การแก้ไขความคิดเห็นข้างต้นของฉัน: "... เนื่องจาก layoutSubviews มีไว้เพื่อลบล้างหรือเรียกจากตัวเอง แต่ไม่ได้หมายถึงการเรียกจากหรือไปยังมุมมองอื่น ... "
Tarfa

[subview1 layoutSubviews]ฉันจริงๆไม่แน่ใจเกี่ยวกับ อาจเป็นไปได้ว่าdrawRectคุณไม่ควรเรียกมันโดยตรง แต่ฉันไม่แน่ใจว่าการโทรsetNeedsLayoutระหว่างขั้นตอนการจัดวางจะทำให้มุมมองถูกจัดวางในช่วงเลย์เอาต์เดียวกันหรือไม่หรือล่าช้าไปจนถึงช่วงถัดไป มันจะดีจะเห็นคำตอบจากใครสักคนจริงๆเข้าใจว่างานนี้ทั้งหมด ...
n8gray

34

ฉันต้องการเพิ่มคำตอบของ n8gray ว่าในบางกรณีคุณจะต้องโทรsetNeedsLayoutตามด้วยlayoutIfNeededตามมาด้วย

ตัวอย่างเช่นสมมติว่าคุณเขียนมุมมองแบบกำหนดเองเพื่อขยาย UIView ซึ่งการวางตำแหน่งของมุมมองย่อยนั้นซับซ้อนและไม่สามารถทำได้ด้วย autoresizingMask หรือ iOS6 AutoLayout layoutSubviewsการวางตำแหน่งที่กำหนดเองสามารถทำได้โดยการเอาชนะ

ตัวอย่างเช่นสมมติว่าคุณมีมุมมองแบบกำหนดเองที่มีcontentViewคุณสมบัติและedgeInsetsคุณสมบัติที่อนุญาตให้ตั้งค่าระยะขอบรอบ ๆ contentView layoutSubviewsจะมีลักษณะดังนี้:

- (void) layoutSubviews {
    self.contentView.frame = CGRectMake(
        self.bounds.origin.x + self.edgeInsets.left,
        self.bounds.origin.y + self.edgeInsets.top,
        self.bounds.size.width - self.edgeInsets.left - self.edgeInsets.right,
        self.bounds.size.height - self.edgeInsets.top - self.edgeInsets.bottom); 
}

หากคุณต้องการทำให้การเปลี่ยนแปลงเฟรมเคลื่อนไหวได้ทุกครั้งที่คุณเปลี่ยนedgeInsetsคุณสมบัติคุณต้องแทนที่ตัวedgeInsetsตั้งค่าดังต่อไปนี้และเรียกsetNeedsLayoutตามด้วยlayoutIfNeeded:

- (void) setEdgeInsets:(UIEdgeInsets)edgeInsets {
    _edgeInsets = edgeInsets;
    [self setNeedsLayout]; //Indicates that the view needs to be laid out 
                           //at next update or at next call of layoutIfNeeded, 
                           //whichever comes first 
    [self layoutIfNeeded]; //Calls layoutSubviews if flag is set
}

ด้วยวิธีนี้หากคุณทำสิ่งต่อไปนี้หากคุณเปลี่ยนคุณสมบัติ edgeInsets ภายในบล็อกแอนิเมชั่นการเปลี่ยนเฟรมของ contentView จะเป็นแบบเคลื่อนไหว

[UIView animateWithDuration:2 animations:^{
    customView.edgeInsets = UIEdgeInsetsMake(45, 17, 18, 34);
}];

หากคุณไม่ได้เพิ่มการเรียกไปยัง layoutIfNeeded ในเมธอด setEdgeInsets ภาพเคลื่อนไหวจะไม่ทำงานเนื่องจาก layoutSubviews จะถูกเรียกในรอบการอัปเดตถัดไปซึ่งเท่ากับการเรียกมันนอกบล็อกภาพเคลื่อนไหว

ถ้าคุณเรียกใช้เฉพาะ layoutIfNeeded ในเมธอด setEdgeInsets จะไม่มีอะไรเกิดขึ้นเนื่องจากไม่ได้ตั้งค่าแฟล็ก setNeedsLayout


4
หรือคุณควรจะสามารถโทรได้[self layoutIfNeeded]ภายในบล็อกภาพเคลื่อนไหวหลังจากตั้งค่าการแทรกขอบ
Scott Fister
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.