วิธีใดที่มีประสิทธิภาพที่สุดในการบังคับให้ UIView วาดใหม่


117

ฉันมี UITableView พร้อมรายการ การเลือกรายการจะผลัก viewController จากนั้นดำเนินการต่อไปนี้ จากเมธอด viewDidLoad ฉันปิดการร้องขอ URLRequest สำหรับข้อมูลที่ต้องการโดยในมุมมองย่อยของฉัน - คลาสย่อย UIView ที่มีการแทนที่ drawRect เมื่อข้อมูลมาจากคลาวด์ฉันจะเริ่มสร้างลำดับชั้นมุมมองของฉัน คลาสย่อยที่เป็นปัญหาจะถูกส่งผ่านข้อมูลและตอนนี้เมธอด drawRect มีทุกสิ่งที่จำเป็นในการแสดงผล

แต่.

เพราะฉันไม่เรียก drawRect อย่างชัดเจน - Cocoa-Touch จัดการอย่างนั้น - ฉันไม่มีทางแจ้ง Cocoa-Touch ว่าฉันต้องการให้คลาสย่อย UIView นี้แสดงผลจริงๆ เมื่อไหร่? ตอนนี้จะดี!

ฉันได้ลอง [myView setNeedsDisplay] แล้ว แบบนี้ใช้งานได้บางครั้ง ขาด ๆ หาย ๆ มาก

ฉันต่อสู้กับสิ่งนี้มาหลายชั่วโมงแล้ว ขอใครสักคนที่กรุณาให้แนวทางที่มั่นคงและรับประกันว่าจะบังคับให้ UIView re-render

นี่คือตัวอย่างโค้ดที่ดึงข้อมูลไปยังข้อมูลพร็อพเพอร์ตี้:

// Create the subview
self.chromosomeBlockView = [[[ChromosomeBlockView alloc] initWithFrame:frame] autorelease];

// Set some properties
self.chromosomeBlockView.sequenceString     = self.sequenceString;
self.chromosomeBlockView.nucleotideBases    = self.nucleotideLettersDictionary;

// Insert the view in the view hierarchy
[self.containerView          addSubview:self.chromosomeBlockView];
[self.containerView bringSubviewToFront:self.chromosomeBlockView];

// A vain attempt to convince Cocoa-Touch that this view is worthy of being displayed ;-)
[self.chromosomeBlockView setNeedsDisplay];

ไชโยดั๊ก

คำตอบ:


193

รับประกัน, ร็อคที่เป็นของแข็งวิธีที่จะบังคับให้ไปอีกครั้งทำให้เป็นUIView [myView setNeedsDisplay]หากคุณกำลังประสบปัญหาคุณอาจประสบปัญหาเหล่านี้:

  • คุณกำลังเรียกมันก่อนที่คุณจะมีข้อมูลจริงหรือคุณ-drawRect:กำลังแคชข้อมูลบางอย่างมากเกินไป

  • คุณคาดหวังว่ามุมมองจะวาดขึ้นในขณะที่คุณเรียกวิธีนี้ ไม่มีเจตนาที่จะเรียกร้อง "วาดตอนนี้วินาทีนี้" โดยใช้ระบบการวาดโกโก้ สิ่งนี้จะรบกวนระบบการจัดองค์ประกอบมุมมองทั้งหมดประสิทธิภาพการทำงานและการสร้างสิ่งประดิษฐ์ทุกประเภท มีเพียงวิธีเดียวที่จะพูดว่า "สิ่งนี้จะต้องถูกจับสลากในรอบการออกรางวัลถัดไป"

หากสิ่งที่คุณต้องการคือ "ตรรกะบางอย่างวาดบางตรรกะเพิ่มเติม" คุณต้องใส่ "ตรรกะเพิ่มเติม" ในวิธีการแยกต่างหากและเรียกใช้โดยใช้-performSelector:withObject:afterDelay:ความล่าช้าเป็น 0 ซึ่งจะทำให้ "ตรรกะอื่น ๆ เพิ่มขึ้น" หลังจาก รอบการออกรางวัลถัดไป ดูคำถามนี้สำหรับตัวอย่างของรหัสประเภทนั้นและกรณีที่อาจจำเป็น (แม้ว่าโดยปกติแล้วควรมองหาวิธีแก้ปัญหาอื่น ๆ หากเป็นไปได้เนื่องจากรหัสนั้นซับซ้อน)

หากคุณไม่คิดว่าสิ่งต่างๆกำลังเกิดขึ้นให้ใส่เบรกพอยต์-drawRect:และดูว่าคุณถูกเรียกเมื่อใด หากคุณกำลังโทร-setNeedsDisplayแต่-drawRect:ไม่ได้รับการเรียกในลูปเหตุการณ์ถัดไปให้เจาะลึกลงไปในลำดับชั้นมุมมองของคุณและตรวจสอบว่าคุณไม่ได้พยายามชิงไหวชิงพริบ ความฉลาดเกินเป็นสาเหตุอันดับ 1 ของการวาดภาพที่ไม่ดีในประสบการณ์ของฉัน เมื่อคุณคิดว่าคุณรู้วิธีที่ดีที่สุดในการหลอกล่อให้ระบบทำในสิ่งที่คุณต้องการคุณมักจะทำให้ระบบทำในสิ่งที่คุณไม่ต้องการ


Rob นี่คือรายการตรวจสอบของฉัน 1) ฉันมีข้อมูลหรือไม่ ใช่. ฉันสร้างมุมมองในเมธอด - hideSpinner - เรียกบนเธรดหลักจาก connectionDidFinishLoading: ด้วยเหตุนี้: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) ฉันไม่ต้องการวาดทันที ฉันแค่ต้องการมัน ในวันนี้ ตอนนี้มันเป็นแบบสุ่มอย่างสมบูรณ์และอยู่นอกเหนือการควบคุมของฉัน [myView setNeedsDisplay] ไม่น่าเชื่อถืออย่างสิ้นเชิง ฉันไปไกลถึงขนาดเรียก [myView setNeedsDisplay] ใน viewDidAppear :. อะ โกโก้ไม่สนใจฉัน ฉุน !!
dugla

1
ฉันพบว่าด้วยวิธีนี้มักจะมีความล่าช้าระหว่างและสายที่แท้จริงของsetNeedsDisplay drawRect:ในขณะที่ความน่าเชื่อถือในการโทรอยู่ที่นี่ฉันจะไม่เรียกสิ่งนี้ว่าโซลูชันที่ "แข็งแกร่งที่สุด" - ตามทฤษฎีแล้วโซลูชันการวาดที่มีประสิทธิภาพควรดำเนินการวาดทั้งหมดทันทีก่อนที่จะกลับไปที่รหัสไคลเอ็นต์ที่ต้องการการดึง
Slipp D.Thompson

1
ฉันแสดงความคิดเห็นด้านล่างสำหรับคำตอบของคุณพร้อมรายละเอียดเพิ่มเติม การพยายามบังคับให้ระบบวาดภาพทำงานผิดปกติโดยการเรียกวิธีการที่เอกสารระบุไว้อย่างชัดเจนว่าไม่โทรไม่ได้มีประสิทธิภาพ ที่ดีที่สุดคือพฤติกรรมที่ไม่ได้กำหนด ส่วนใหญ่จะทำให้ประสิทธิภาพและคุณภาพการวาดลดลง อย่างเลวร้ายที่สุดอาจเกิดการชะงักงันหรือผิดพลาด
Rob Napier

1
หากคุณต้องการวาดก่อนกลับให้วาดลงในบริบทรูปภาพ (ซึ่งใช้งานได้เหมือนกับผืนผ้าใบที่หลายคนคาดหวัง) แต่อย่าพยายามหลอกระบบคอมโพสิต ได้รับการออกแบบมาเพื่อให้กราฟิกคุณภาพสูงอย่างรวดเร็วโดยมีค่าใช้จ่ายทรัพยากรขั้นต่ำ ส่วนหนึ่งคือการรวมขั้นตอนการวาดที่ส่วนท้ายของลูปรัน
Rob Napier

1
@RobNapier ฉันไม่เข้าใจว่าทำไมคุณถึงเป็นฝ่ายรับ โปรดตรวจสอบอีกครั้ง; ไม่มีการละเมิด API (แม้ว่าจะได้รับการทำความสะอาดตามคำแนะนำของคุณ แต่ฉันขอยืนยันว่าการเรียกวิธีการของตัวเองไม่ใช่การละเมิด) และไม่มีโอกาสที่จะหยุดชะงักหรือขัดข้อง นอกจากนี้ยังไม่ใช่ "เคล็ดลับ"; มันใช้ setNeedsDisplay / displayIfNeeded ของ CALayer ตามแบบปกติ นอกจากนี้ฉันใช้มันมาระยะหนึ่งแล้วเพื่อวาดด้วย Quartz ในลักษณะขนานกับ GLKView / OpenGL ได้รับการพิสูจน์แล้วว่าปลอดภัยเสถียรและเร็วกว่าโซลูชันที่คุณระบุไว้ ถ้าคุณไม่เชื่อฉันลองดูสิ คุณไม่มีอะไรจะเสียที่นี่
Slipp D.Thompson

52

ฉันมีปัญหากับความล่าช้าอย่างมากระหว่างการเรียก setNeedsDisplay และ drawRect: (5 วินาที) ปรากฎว่าฉันเรียก setNeedsDisplay ในเธรดอื่นที่ไม่ใช่เธรดหลัก หลังจากย้ายสายนี้ไปยังเธรดหลักความล่าช้าก็หายไป

หวังว่านี่จะเป็นประโยชน์บ้าง


นี่เป็นข้อบกพร่องของฉันแน่นอน ฉันรู้สึกว่ากำลังทำงานบนเธรดหลัก แต่มันไม่ได้จนกว่าฉันจะ NSLogged NSThread.isMainThread ฉันรู้ว่ามีกรณีมุมที่ฉันไม่ได้ทำการเปลี่ยนแปลงเลเยอร์บนเธรดหลัก ขอบคุณที่ช่วยฉันจากการดึงผมออก!
นาย T

14

วิธีที่รับประกันคืนเงินคอนกรีตเสริมเหล็กแข็งเพื่อบังคับให้มุมมองวาดแบบซิงโครนัส (ก่อนกลับไปที่รหัสการโทร)คือการกำหนดค่าการCALayerโต้ตอบกับUIViewคลาสย่อยของคุณ

ในคลาสย่อย UIView ของคุณให้สร้างdisplayNow()เมธอดที่บอกให้เลเยอร์“ กำหนดหลักสูตรสำหรับการแสดงผล ” จากนั้นจึง“ ทำให้เป็นเช่นนั้น ”:

รวดเร็ว

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
public func displayNow()
{
    let layer = self.layer
    layer.setNeedsDisplay()
    layer.displayIfNeeded()
}

Objective-C

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
- (void)displayNow
{
    CALayer *layer = self.layer;
    [layer setNeedsDisplay];
    [layer displayIfNeeded];
}

ใช้draw(_: CALayer, in: CGContext)วิธีการที่จะเรียกวิธีการวาดส่วนตัว / ภายในของคุณ(ซึ่งใช้ได้ผลเนื่องจากทุกอย่างUIViewเป็น a CALayerDelegate) :

รวดเร็ว

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
override func draw(_ layer: CALayer, in context: CGContext)
{
    UIGraphicsPushContext(context)
    internalDraw(self.bounds)
    UIGraphicsPopContext()
}

Objective-C

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
    UIGraphicsPushContext(context);
    [self internalDrawWithRect:self.bounds];
    UIGraphicsPopContext();
}

และสร้างinternalDraw(_: CGRect)วิธีการที่คุณกำหนดเองพร้อมกับความปลอดภัยที่ไม่ปลอดภัยdraw(_: CGRect):

รวดเร็ว

/// Internal drawing method; naming's up to you.
func internalDraw(_ rect: CGRect)
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
override func draw(_ rect: CGRect) {
    internalDraw(rect)
}

Objective-C

/// Internal drawing method; naming's up to you.
- (void)internalDrawWithRect:(CGRect)rect
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
- (void)drawRect:(CGRect)rect {
    [self internalDrawWithRect:rect];
}

และตอนนี้เพียงโทรmyView.displayNow()เมื่อใดก็ตามที่คุณจริงๆจริงๆต้องไปวาด(เช่นจากCADisplayLinkการติดต่อกลับ) displayNow()วิธีการของเราจะบอกถึงCALayerถึงdisplayIfNeeded()ซึ่งจะโทรกลับเข้ามาdraw(_:,in:)พร้อมกันและทำการวาดในการinternalDraw(_:)อัปเดตภาพด้วยสิ่งที่ดึงเข้ามาในบริบทก่อนที่จะดำเนินการต่อ


วิธีนี้คล้ายกับของ @ RobNapier ข้างต้น แต่มีข้อดีของการโทรdisplayIfNeeded()นอกเหนือไปจากsetNeedsDisplay()ซึ่งทำให้ซิงโครนัส

สิ่งนี้เป็นไปได้เนื่องจากCALayers แสดงฟังก์ชันการวาดมากกว่าUIViewที่ทำ - เลเยอร์อยู่ในระดับที่ต่ำกว่ามุมมองและได้รับการออกแบบอย่างชัดเจนเพื่อจุดประสงค์ในการวาดที่กำหนดค่าได้สูงภายในเค้าโครงและ (เช่นเดียวกับหลาย ๆ อย่างใน Cocoa) ได้รับการออกแบบให้ใช้งานได้อย่างยืดหยุ่น ( ในฐานะคลาสแม่หรือในฐานะผู้แทนหรือเป็นสะพานเชื่อมไปยังระบบการวาดภาพอื่น ๆ หรือเพียงแค่ของพวกเขาเอง) การใช้งานไฟล์CALayerDelegateโปรโตคอลอย่างเหมาะสมทำให้ทั้งหมดนี้เป็นไปได้

ข้อมูลเพิ่มเติมเกี่ยวกับ configurability ของCALayers สามารถพบได้ในการตั้งค่าชั้นส่วนของคอร์นิเมชั่นการเขียนโปรแกรมคู่มือวัตถุ


โปรดทราบว่าเอกสารประกอบdrawRect:ระบุไว้อย่างชัดเจนว่า "คุณไม่ควรเรียกวิธีนี้โดยตรงด้วยตัวเอง" นอกจากนี้ CALayer ยังdisplayบอกอย่างชัดเจนว่า "อย่าเรียกวิธีนี้โดยตรง" หากคุณต้องการวาดพร้อมกันบนเลเยอร์contentsโดยตรงไม่จำเป็นต้องละเมิดกฎเหล่านี้ คุณสามารถวาดลงบนเลเยอร์ได้contentsตลอดเวลาที่คุณต้องการ (แม้ในเธรดพื้นหลัง) เพียงแค่เพิ่มชั้นย่อยลงในมุมมองสำหรับสิ่งนั้น แต่แตกต่างจากการวางบนหน้าจอซึ่งต้องรอจนกว่าจะถึงเวลาประกอบที่ถูกต้อง
Rob Napier

ฉันบอกว่าจะเพิ่มเลเยอร์ย่อยเนื่องจากเอกสารยังเตือนเกี่ยวกับการยุ่งกับเลเยอร์ของ UIView โดยตรงcontents("หากวัตถุเลเยอร์เชื่อมโยงกับวัตถุมุมมองคุณควรหลีกเลี่ยงการตั้งค่าเนื้อหาของคุณสมบัตินี้โดยตรงการทำงานร่วมกันระหว่างมุมมองและเลเยอร์มักจะส่งผล ในมุมมองแทนที่เนื้อหาของคุณสมบัตินี้ในระหว่างการอัปเดตในภายหลัง ") ฉันไม่แนะนำวิธีนี้เป็นพิเศษ การวาดก่อนกำหนดจะทำลายประสิทธิภาพและคุณภาพการวาด แต่ถ้าคุณต้องการด้วยเหตุผลบางอย่างแล้วcontentsจะทำอย่างไร
Rob Napier

@RobNapier Point ถ่ายด้วยโทรdrawRect:โดยตรง. ไม่จำเป็นต้องสาธิตเทคนิคนี้และได้รับการแก้ไขแล้ว
Slipp D.Thompson

@RobNapier สำหรับcontentsเทคนิคที่คุณแนะนำ ... ฟังดูน่าหลงใหล ฉันลองทำอะไรแบบนั้นในตอนแรก แต่ไม่สามารถใช้งานได้และพบว่าวิธีแก้ปัญหาข้างต้นเป็นโค้ดที่น้อยกว่ามากโดยไม่มีเหตุผลที่จะไม่เหมือนกับประสิทธิภาพ อย่างไรก็ตามหากคุณมีวิธีแก้ปัญหาที่ใช้ได้ผลcontentsฉันก็สนใจที่จะอ่านมัน (ไม่มีเหตุผลว่าทำไมคุณไม่มีคำตอบสองข้อสำหรับคำถามนี้ใช่ไหม)
Slipp D. Thompson

@RobNapier หมายเหตุ: นี่ไม่ได้มีอะไรจะทำอย่างไรกับ displayCALayer เป็นเพียงวิธีการสาธารณะที่กำหนดเองในคลาสย่อย UIView เช่นเดียวกับสิ่งที่ทำใน GLKView (คลาสย่อย UIView อื่นและสิ่งเดียวที่เขียนโดย Apple เท่านั้นที่ฉันรู้ว่าต้องการฟังก์ชันDRAW NOW! )
Slipp D.Thompson

5

ฉันมีปัญหาเดียวกันและวิธีแก้ปัญหาทั้งหมดจาก SO หรือ Google ไม่ได้ผลสำหรับฉัน โดยปกติแล้วsetNeedsDisplayจะได้ผล แต่เมื่อมันไม่ได้ ...
ฉันได้ลองเรียกsetNeedsDisplayดูจากทุก ๆ เธรดและทุกสิ่งที่เป็นไปได้ - ก็ยังไม่ประสบความสำเร็จ เรารู้อย่างที่ร็อบพูดนั่นแหละ

"สิ่งนี้จะต้องถูกจับฉลากในรอบการออกรางวัลถัดไป"

แต่ด้วยเหตุผลบางอย่างมันจะไม่เกิดขึ้นในครั้งนี้ และวิธีแก้ปัญหาเดียวที่ฉันพบคือเรียกมันด้วยตนเองหลังจากผ่านไประยะหนึ่งเพื่อปล่อยให้สิ่งที่ขวางกั้นการจับฉลากผ่านไปเช่นนี้:

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 
                                        (int64_t)(0.005 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
    [viewToRefresh setNeedsDisplay];
});

เป็นทางออกที่ดีหากคุณไม่ต้องการมุมมองในการวาดใหม่บ่อยนัก มิฉะนั้นถ้าคุณกำลังทำบางอย่างเคลื่อนไหว (การกระทำ) สิ่งที่มักจะมีปัญหาใด ๆ setNeedsDisplayกับเพียงแค่โทร

ฉันหวังว่ามันจะช่วยคนที่หลงทางเหมือนฉัน


0

ฉันรู้ดีว่านี่อาจเป็นการเปลี่ยนแปลงครั้งใหญ่หรือไม่เหมาะกับโครงการของคุณ แต่คุณคิดว่าจะไม่ดำเนินการผลักดันจนกว่าคุณจะมีข้อมูลอยู่แล้วหรือไม่? ด้วยวิธีนี้คุณจะต้องวาดมุมมองเพียงครั้งเดียวและประสบการณ์ของผู้ใช้ก็จะดีขึ้นเช่นกัน - การกดจะย้ายเข้ามาแล้ว

วิธีที่คุณทำคือUITableView didSelectRowAtIndexPathคุณขอข้อมูลแบบอะซิงโครนัส เมื่อคุณได้รับการตอบสนองคุณด้วยตนเองดำเนินการทำต่อและส่งข้อมูลไปยัง viewController prepareForSegueคุณใน ในขณะเดียวกันคุณอาจต้องการแสดงตัวบ่งชี้กิจกรรมบางอย่างสำหรับการตรวจสอบตัวบ่งชี้การโหลดอย่างง่ายhttps://github.com/jdg/MBProgressHUD

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