UIScrollView หยุด NSTimer ชั่วคราวจนกว่าการเลื่อนจะเสร็จสิ้น


84

ในขณะที่UIScrollView(หรือคลาสที่ได้รับมา) กำลังเลื่อนดูเหมือนว่าทุกสิ่งNSTimersที่กำลังทำงานอยู่จะหยุดชั่วคราวจนกว่าการเลื่อนจะเสร็จสิ้น

มีวิธีแก้ปัญหานี้หรือไม่? เธรด? การตั้งค่าลำดับความสำคัญ? อะไรมั้ย?



เจ็ดปีต่อมา ... stackoverflow.com/a/12625429/294884
Fattie

คำตอบ:


202

วิธีแก้ปัญหาที่ง่ายและใช้งานง่ายคือทำ:

NSTimer *timer = [NSTimer timerWithTimeInterval:... 
                                         target:...
                                       selector:....
                                       userInfo:...
                                        repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

2
UITrackingRunLoopMode เป็นโหมด - และเป็นแบบสาธารณะ - คุณสามารถใช้เพื่อสร้างพฤติกรรมการจับเวลาต่างๆ
Tom Andersen

รักมันใช้งานได้อย่างมีเสน่ห์ด้วย GLKViewController เช่นกัน เพียงตั้งค่า controller.paused เป็น YES เมื่อ UIScrollView ปรากฏขึ้นและเริ่มจับเวลาของคุณเองตามที่อธิบายไว้ ย้อนกลับเมื่อ scrollview ถูกปิดและนั่นคือมัน! การอัปเดต / การแสดงผลของฉันไม่แพงมากดังนั้นอาจช่วยได้
Jeroen Bouma

3
สุดยอด! ฉันต้องถอดตัวจับเวลาออกเมื่อทำไม่ถูกต้องหรือไม่? ขอบคุณ
Jacopo Penzo

ใช้สำหรับการเลื่อน แต่หยุดตัวจับเวลาเมื่อแอปเข้าสู่พื้นหลัง วิธีแก้ปัญหาใด ๆ สำหรับการบรรลุทั้งสองอย่าง?
Pulkit Sharma

23

สำหรับใครที่ใช้ Swift 3

timer = Timer.scheduledTimer(timeInterval: 0.1,
                            target: self,
                            selector: aSelector,
                            userInfo: nil,
                            repeats: true)


RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)

7
เป็นการดีกว่าที่จะเรียกtimer = Timer(timeInterval: 0.1, target: self, selector: aSelector, userInfo: nil, repeats: true)เป็นคำสั่งแรกแทนTimer.scheduleTimer()เนื่องจากscheduleTimer()เพิ่มตัวจับเวลาให้กับ runloop และการเรียกครั้งต่อไปเป็นการเพิ่มอีกครั้งใน runloop เดียวกัน แต่ใช้โหมดที่แตกต่างกัน อย่าทำงานเดียวกันซ้ำสอง
Accid Bright

8

ใช่พอลพูดถูกนี่เป็นปัญหาการวนซ้ำ โดยเฉพาะคุณต้องใช้ประโยชน์จากวิธี NSRunLoop:

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode


6

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


11
ไม่ต้องใช้เธรดอื่นดูคำตอบล่าสุดจาก Kashif ฉันลองแล้วและใช้งานได้โดยไม่ต้องใช้เธรดเพิ่มเติม
progrmr

3
ยิงปืนใหญ่ใส่นกกระจอก แทนที่จะเป็นเธรดเพียงตั้งเวลาจับเวลาในโหมดวนรอบการทำงานด้านขวา
uliwitness

2
ฉันคิดว่าคำตอบของ Kashif ด้านล่างนี้เป็นคำตอบที่ดีที่สุดเพราะคุณต้องเพิ่มโค้ด 1 บรรทัดเพื่อแก้ไขปัญหานี้
เมียนเมอร์ฟี่

3

สำหรับทุกคนที่ใช้ Swift 4:

    timer = Timer(timeInterval: 1, target: self, selector: #selector(timerUpdated), userInfo: nil, repeats: true)
    RunLoop.main.add(timer, forMode: .common)

2

tl; dr runloop กำลังทำการเลื่อนดังนั้นจึงไม่สามารถจัดการกับเหตุการณ์ใด ๆ ได้อีกเว้นแต่คุณจะตั้งค่าตัวจับเวลาด้วยตนเองเพื่อให้สามารถเกิดขึ้นได้เมื่อ runloop จัดการกับเหตุการณ์การสัมผัส หรือลองใช้วิธีอื่นและใช้ GCD


ต้องอ่านสำหรับนักพัฒนา iOS ทุกคน ในที่สุดสิ่งต่างๆมากมายถูกดำเนินการผ่าน RunLoop

ได้มาจากเอกสารของ Appleเอกสารของแอปเปิ้ล

Run Loop คืออะไร?

การวนรอบเป็นอย่างมากเหมือนกับชื่อของมัน เป็นการวนซ้ำที่เธรดของคุณป้อนและใช้เพื่อเรียกใช้ตัวจัดการเหตุการณ์เพื่อตอบสนองต่อเหตุการณ์ที่เข้ามา

การจัดส่งเหตุการณ์หยุดชะงักอย่างไร

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

จะเกิดอะไรขึ้นถ้าตัวจับเวลาเริ่มทำงานเมื่อรันลูปอยู่ระหว่างการดำเนินการ

สิ่งนี้เกิดขึ้นหลายครั้งโดยที่เราไม่เคยสังเกตเห็น ฉันหมายความว่าเราตั้งเวลาให้ยิงเวลา 10: 10: 10: 00 น แต่ runloop กำลังดำเนินการเหตุการณ์ซึ่งใช้เวลาจนถึง 10: 10: 10: 05 ดังนั้นตัวจับเวลาจะยิง 10: 10: 10: 06

ในทำนองเดียวกันถ้าตัวจับเวลาเริ่มทำงานเมื่อลูปรันอยู่ระหว่างการดำเนินการตามรูทีนตัวจัดการตัวจับเวลาจะรอจนกว่าจะถึงครั้งต่อไปที่รันลูปเพื่อเรียกใช้รูทีนตัวจัดการ หากลูปการวิ่งไม่ทำงานเลยตัวจับเวลาจะไม่เริ่มทำงาน

การเลื่อนหรืออะไรก็ตามที่ทำให้ runloop ไม่ว่างกะตลอดเวลาที่ตัวจับเวลาของฉันจะเริ่มทำงานหรือไม่

คุณสามารถกำหนดค่าตัวจับเวลาเพื่อสร้างเหตุการณ์เพียงครั้งเดียวหรือซ้ำ ๆ ตัวจับเวลาที่เกิดซ้ำจะกำหนดเวลาใหม่โดยอัตโนมัติตามเวลาการยิงที่กำหนดไว้ไม่ใช่เวลาการยิงจริง ตัวอย่างเช่นหากตัวจับเวลาถูกกำหนดให้เริ่มทำงานในช่วงเวลาใดเวลาหนึ่งและทุกๆ 5 วินาทีหลังจากนั้นเวลายิงที่กำหนดไว้จะตกตามช่วงเวลาเดิม 5 วินาทีเสมอแม้ว่าเวลาในการยิงจริงจะล่าช้าก็ตาม หากเวลายิงล่าช้ามากจนพลาดเวลาการยิงที่กำหนดไว้อย่างน้อยหนึ่งครั้งตัวจับเวลาจะยิงเพียงครั้งเดียวสำหรับช่วงเวลาที่พลาดไป หลังจากยิงไปในช่วงเวลาที่พลาดไปตัวจับเวลาจะถูกกำหนดใหม่สำหรับเวลาการยิงที่กำหนดไว้ถัดไป

ฉันจะเปลี่ยนโหมดของ RunLoops ได้อย่างไร?

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


วิธีการแก้:

trackingเมื่อผู้ใช้เลื่อนการเรียกใช้โหมดห่วงกลายเป็น RunLoop ออกแบบมาเพื่อเปลี่ยนเกียร์ เมื่อตั้งค่าโหมดเป็นeventTrackingแล้วจะให้ความสำคัญ (จำไว้ว่าเรามีแกน CPU ที่ จำกัด ) เพื่อสัมผัสเหตุการณ์ นี่คือการออกแบบสถาปัตยกรรมโดยนักออกแบบระบบปฏิบัติการนี่คือการออกแบบสถาปัตยกรรมโดยนักออกแบบระบบปฏิบัติการ

ตามค่าเริ่มต้นตัวจับเวลาจะไม่ถูกกำหนดไว้ในtrackingโหมด มีกำหนดใน:

สร้างตัวจับเวลาและกำหนดเวลาบนลูปรันปัจจุบันใน โหมดเริ่มต้น

scheduledTimerใต้ไม่นี้:

RunLoop.main.add(timer, forMode: .default)

หากคุณต้องการให้ตัวจับเวลาของคุณทำงานเมื่อเลื่อนคุณต้องทำอย่างใดอย่างหนึ่ง:

let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self,
 selector: #selector(fireTimer), userInfo: nil, repeats: true) // sets it on `.default` mode

RunLoop.main.add(timer, forMode: .tracking) // AND Do this

หรือเพียงแค่ทำ:

RunLoop.main.add(timer, forMode: .common)

ในที่สุดการทำอย่างใดอย่างหนึ่งข้างต้นหมายความว่าเธรดของคุณจะไม่ถูกบล็อกโดยเหตุการณ์การสัมผัส ซึ่งเทียบเท่ากับ:

RunLoop.main.add(timer, forMode: .default)
RunLoop.main.add(timer, forMode: .eventTracking)
RunLoop.main.add(timer, forMode: .modal) // This is more of a macOS thing for when you have a modal panel showing.

ทางเลือกอื่น:

คุณอาจพิจารณาใช้ GCD เป็นตัวจับเวลาซึ่งจะช่วยให้คุณ "ป้องกัน" โค้ดของคุณจากปัญหาการจัดการลูป

สำหรับการไม่ทำซ้ำให้ใช้:

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    // your code here
}

สำหรับการจับเวลาซ้ำให้ใช้:

ดูวิธีใช้ DispatchSourceTimer


เจาะลึกจากการสนทนาที่ฉันมีกับ Daniel Jalkut:

คำถาม: GCD (เธรดพื้นหลัง) เช่น asyncAfter บนเธรดพื้นหลังถูกเรียกใช้งานนอก RunLoop ได้อย่างไร ความเข้าใจของฉันจากนี้คือทุกอย่างจะต้องดำเนินการภายใน RunLoop

ไม่จำเป็น - ทุกเธรดจะมีลูปการรันมากที่สุดเพียงหนึ่งครั้ง แต่สามารถมีศูนย์ได้หากไม่มีเหตุผลที่จะประสานการดำเนินการ "ความเป็นเจ้าของ" ของเธรด

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

โดยปกติถ้าคุณส่งบางสิ่งที่รันบนเธรดมันอาจจะไม่มี runloop เว้นแต่จะมีการเรียกใช้ [NSRunLoop currentRunLoop]ซึ่งจะสร้างขึ้นโดยปริยาย

โดยสรุปแล้วโหมดต่างๆเป็นกลไกการกรองสำหรับอินพุตและตัวจับเวลา

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