Bibi's DevLog ๐ค๐
[Swift] delay : ์ง์ฐ์๊ฐ ๋๊ณ ์ฝ๋ (๋ฐ๋ณต) ์คํํ๊ธฐ - Timer, asyncAfter ๋ณธ๋ฌธ
[Swift] delay : ์ง์ฐ์๊ฐ ๋๊ณ ์ฝ๋ (๋ฐ๋ณต) ์คํํ๊ธฐ - Timer, asyncAfter
๋น๋น bibi 2022. 12. 2. 23:10์ฐธ๊ณ ํ ๋ฌธ์
Why would a scheduledTimer
fire properly when setup outside a block, but not within a block?
Swift์์ ์ง์ฐ์๊ฐ์ ๋๊ณ ์ฝ๋๋ฅผ (๋ฐ๋ณต)์คํํ๋ ๋ฐฉ๋ฒ์ ํฌ๊ฒ ๋ ๊ฐ์ง๊ฐ ์๋๋ฐ,
Timer
์ DispatchQueue.main.asyncAfter
์ด๋ค.
Note:
Before I start, I want to make it clear that there is a significant energy cost to using timers. Weโll look at ways to mitigate this, but broadly any kind of timer must wake the system from its idle state in order to trigger work, and that has an associated energy cost.
ํ์ด๋จธ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์๋นํ ์๋์ง๋ฅผ ์๋ชจํ๋ ์์ ์์ ์์์ผ ํ๋ค. ๋ฌผ๋ก ์ํํ๋ ๋ฐฉ๋ฒ๋ ์๋ค.
1. Timer
๊ธฐ๋ณธ์ ์ธ ๋ฐ๋ณต ํ์ด๋จธ ์ฌ์ฉ์
Timer.scheduledTimer
๋ก ๊ฐ๋ฅํ๋ค.// ์ ๋ ํฐ๋ก ๋๊ธฐ๊ธฐ - ํ์ด๋จธ๋ฅผ ๋ณ์๋ก ๋นผ์ ์ฌ์ฉํ๊ธฐ ์ข๋ค let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true) @objc func fireTimer() { print("Timer fired!") } // ๋๋ ํด๋ก์ ๋ก ๋ฐ๋ณต์ํ ๋์์ ๋๊ธธ ์๋ ์์ - inline์ผ๋ก ์ฌ์ฉํ๊ธฐ ์ข๋ค let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in print("Timer fired!") }
- ํ์ด๋จธ๋ฅผ ํ๋กํผํฐ๋ก ๋ง๋ค์ด ๋๋ฉด ๋์ค์ ์ข ๋ฃํ ๋ ์ ์ฉํ๋ค.
๋ฐ๋ณตํ์ง ์๋ ํ์ด๋จธ -
repeates
๋ฅผ false๋ก ํ๋ค.์๋์ผ๋ก ์ค์ค๋ก invalidate๋๋ค.
let timer1 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: false) let timer2 = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in print("Timer fired!") }
ํ์ด๋จธ ์ข ๋ฃ :
invalidate()
๋ฅผ ์ฌ์ฉvar runCount = 0 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in print("Timer fired!") runCount += 1 if runCount == 3 { timer.invalidate() } }
userInfo
์ธ์์ ์ ๋ณด๋ฅผ ์ถ๊ฐํด ํ์ด๋จธ๋ฅผ ๋ฐ๋ํ ๋งฅ๋ฝ์ ์ ์ฅํ ์ ์๋ค. (Any? ํ์ )let context = ["user": "@twostraws"] Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: context, repeats: true) @objc func fireTimer(timer: Timer) { guard let context = timer.userInfo as? [String: String] else { return } let user = context["user", default: "Anonymous"] print("Timer fired by \(user)!") runCount += 1 if runCount == 3 { timer.invalidate() } }
tolerance(ํ์ฉ ์ค์ฐจ)
์์คํ ์ด ์๋ ์ง์ ํ ์๊ฐ๊ณผ ํ์ฉ ์ค์ฐจ ์ฌ์ด์ ์ด๋ ์์ ์๋ ํ์ด๋จธ๋ฅผ ๋ฐ๋(trigger)ํ ์ ์์์ ์๋ฏธ
ํ์ด๋จธ๊ฐ ์๋ชจํ๋ ์๋์ง๋ฅผ ์ค์ผ ์ ์๋ ์ฌ์ด ๋ฐฉ๋ฒ์ด๋ค.
- ์์คํ ์ ์ธ์ ๋ ์๋์ ์ผ๋ก ์ฝ๊ฐ์ ํ์ฉ์ค์ฐจ๋ฅผ ๋๋ค.
tolerance๋ฅผ ์ง์ ํ๋๋ผ๋ ์ ๋๋ก ์ง์ ์๊ฐ ์ด์ ์ ํ์ด๋จธ๋ฅผ ๋ฐ๋ํ์ง๋ ์๋๋ค.
tolerance๋ฅผ ์ง์ ํ๋๋ผ๋ ํ์ด๋จธ๊ฐ drift๋์ง ์๋๋ค (=๋ค์ ํธ๋ฆฌ๊ฑฐ๊ฐ ๋ ๋นจ๋ฆฌ ๋ฐ๋๋์ง ์๋๋ค)
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true) timer.tolerance = 0.2 // default๊ฐ์ 0์.
As an example, consider a timer that was asked to execute every 1 second with a 0.5 second tolerance. It might run like this:
After 1.0 seconds the timer fires.
After 2.4 seconds the timer fires again. Itโs 0.4 seconds late, but thatโs still within our tolerance.
After 3.1 seconds the timer fires again. This is only 0.7 seconds after our previous fire event, but each fire date is calculated from the original regardless of tolerance.
After 4.5 seconds the timer fires again.
And so onโฆ
runloop์ ํจ๊ป ์์ ํ๊ธฐ
โ ์ฌ์ฉ์๊ฐ ์ฑ๊ณผ ์ํธ์์ฉํ๋ ์ค์๋ Timer๊ฐ ๋ฐ๋๋์ง ์๋๋ค.
์? ์ฐ๋ฆฌ๋ ์๋ฌต์ ์ผ๋ก Timer๋ฅผ ๋ฐ๋ฃจํ์
defaultRunLoopMode
์์ ์์ฑํ๊ณ ์์์ด๊ฒ์ ์ฑ์ ๋ฉ์ธ ์ค๋ ๋์์ ์ ํจํ๋ฉฐ, ์ฌ์ฉ์-UI ์ํธ์์ฉ์ด ์ผ์ด๋๋ ์ค์๋ ์ผ์์ค์ง๋๋ค. ์ํธ์์ฉ์ด ๋๋๋ฉด ์ฌ๊ฐ๋๋ค.
๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฐ์ฅ ์ฌ์ด ๋ฐฉ๋ฒ์ ์ง์ ์ ํํ ๋ฐ๋ฃจํ์ ํ์ด๋จธ๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ด๋ค.
- ํ์ด๋จธ๊ฐ ๋ฉ์ธ ์ค๋ ๋์์ ์คํ๋๊ณ ์์ง ์๋ค๋ฉด ๋ฐ๋ฃจํ๋ฅผ ๊ฐ์ง ๋ชปํ๊ธฐ ๋๋ฌธ.
- ํ์ด๋จธ๋ ๋ฐ๋์ ๋ฐ๋ฃจํ๋ฅผ ๊ฐ์ ธ์ผ ํจ. ๋ฉ์ธ ์ค๋ ๋๋ ํ๋์ ๋ฐ๋ฃจํ๋ฅผ ๊ฐ์ง์ง๋ง, ๋๋ถ๋ถ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋๋ ์ง์ ์ถ๊ฐํ๊ธฐ ์ ์๋ ๋ฐ๋ฃจํ๋ฅผ ๊ฐ์ง ์๋๋ค. - ์ฐธ๊ณ
- ํ์ด๋จธ๋ ํ๋ฒ์ ํ๋์ ๋ฐ๋ฃจํ์๋ง ์ถ๊ฐ ๊ฐ๋ฅ. ๋จ, ํด๋น ๋ฐ๋ฃจํ์ ์ฌ๋ฌ ๋ฐ๋ฃจํ๋ชจ๋์๋ ์ถ๊ฐํ ์ ์๋ค.
.common
: UI ์ฌ์ฉ ์ค์๋ ํ์ด๋จธ๊ฐ ๋ฐ๋๋ ์ ์๊ฒ ํด ์ฃผ๋ ๋ฐ๋ฃจํ ๋ชจ๋.// ํ์ด๋จธ ๋์์ด ์๋ ์, ์ง์ ๋ฐ๋ฃจํ์ ํ์ด๋จธ๋ฅผ ๋ํด ์ค๋ค. let context = ["user": "@twostraws"] let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: context, repeats: true) RunLoop.current.add(timer, forMode: .common) // ๋๋ main์ค๋ ๋๋ก ์คํํ๋ ๋ฐฉ๋ฒ๋ ์๋ค DispatchQueue.main.async { self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true) }
ํ๋ฉด ๋ณํ์ ํ์ด๋จธ ๋๊ธฐํํ๊ธฐ
CADisplayLink ๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด ์๋ค. ์๋๋ ๊ฐ๋จํ ์ฝ๋ ์์ . ( ์ฐธ๊ณ )
let displayLink = CADisplayLink(target: self, selector: #selector(fireTimer)) displayLink.add(to: .current, forMode: .default)
ํ์ด๋จธ ๋ฐ๋ ์, UI๊ฐ ์ฌ์ฉ์ค์์๋ ํ๋ฉด๊ณผ ์ฐ๊ฒฐ๋ ๋ฉ์๋๋ฅผ ๋ฐ๋์ํค๊ณ ์ถ๋ค๋ฉด
.common
์ ์ฌ์ฉํ๋ผ.
๋ฐ๋ฃจํ RunLoop
RunLoop ๋ฐ๋ฃจํ๋??
- ๋ฐ๋ฃจํ๋ ์ค๋ ๋์ ์ฐ๊ด๋ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฌผ์ ์ผ๋ถ์ด๋ค.
- ๋ฐ๋ฃจํ๋ ์์ ์ ์์ฝ(schedule)ํ๊ณ , ๋ค์ด์ค๋ ์ด๋ฒคํธ์ ์์ ์ ์กฐ์ ํ ๋ ์ฌ์ฉํ๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ฃจํ์ด๋ค.
- ๋ฐ๋ฃจํ์ ๋ชฉ์ ์ ์ค๋ ๋๊ฐ ์ผํด์ผ ํ ๋๋ ์ผํ๊ณ , ์ผ์ด ์์ ๋๋ ์ฌ๋๋ก ํ๋ ๊ฒ!
- ๋ฐ๋ฃจํ ๊ด๋ฆฌ๋ ์์ ์๋์ด ์๋. ํ์ํ ๋๋ ํ๋ก๊ทธ๋๋จธ๊ฐ ์ง์ ์กฐ์ ํด์ค์ผ ํจ.
- Cocoa์ Core Foundation์ ๋ฐ๋ฃจํ ๊ฐ์ฒด๋ฅผ ์ ๊ณตํ๋ค.
- ๋ฉ์ธ ์ค๋ ๋๋ฅผ ํฌํจํด ๊ฐ ์ค๋ ๋์๋ ๋ชจ๋ ์ฐ๊ด๋ ๋ฐ๋ฃจํ ๊ฐ์ฒด๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์กด์ฌํ๋ค.
- ๋ฐ๋ผ์ ๋ฐ๋ฃจํ ๊ฐ์ฒด๋ฅผ ์ง์ ์์ฑํ ์ผ์ ์๋ค.
- ๋ฉ์ธ ์ค๋ ๋์ ๋ฐ๋ฃจํ๋ ์๋์ ์ผ๋ก ์คํ๋๋ค.
- ์ฑ ํ๋ ์์ํฌ๊ฐ ์ฑ ์์ ํ๋ก์ธ์ค์ ์ผํ์ผ๋ก ์คํํด ์ฃผ๋ ๊ฒ์.
- ๋ฉ์ธ์ด ์๋ ๋ณด์กฐ ์ค๋ ๋๋ค์ ๋ฐ๋ฃจํ๋ฅผ ๋ช ์์ ์ผ๋ก โ์คํ'ํด ์ฃผ์ด์ผ ๋์ํ๋ค.
โ Command Line Tool(macOS) ํ๋ก์ ํธ์์ ํ์ด๋จธ๊ฐ ์คํ๋์ง ์๋ ์ด์
โ CLT์์๋ ๋ฉ์ธ ๋ฐ๋ฃจํ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์คํ๋์ง ์์. ์ฑ ํ๋ ์์ํฌ๋ง์ด ๋ฉ์ธ ๋ฐ๋ฃจํ๋ฅผ ๊ธฐ๋ณธ์ ์ผ๋ก ์คํ ์ํ๋ก ๋ง๋ค์ด ์ค ๋ฟ์. CLT์์๋ ๋ช ์์ ์ผ๋ก ๊ฐ๋ฐ์๊ฐ ๋ฐ๋ฃจํ์ ์คํ์ ํด์ค์ผ ํจ.
- ํ์ด๋จธ๋ ๊ทธ๊ฒ์ด ์๋๋๊ธฐ ์ํ ๋ฐ๋ฃจํ๊ฐ ํ์ํ๋ฐ, CLI ํ๊ฒฝ์ ๋ฐ๋ฃจํ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์คํ๋์ง ์๋ ์ํ์ด๋ค. ๋ช
์์ ์ผ๋ก ์คํ์ด ํ์ํจ
- iOS App ํ๋ก์ ํธ๋ ์ฑ ํ๋ ์์ํฌ๊ฐ ๋ฉ์ธ ์ค๋ ๋์ ๋ฐ๋ฃจํ๋ฅผ ๊ธฐ๋ณธ์ ์ผ๋ก ์คํํด ์ฃผ๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ์คํ์ด ํ์ํ์ง ์์๋ ๊ฒ์.
๋ฐฉ๋ฒ1 - ์์๊ณผ ์ข ๋ฃ ๋ช ์
- ์์ :
CFRunLoopRun()
- ์ข
๋ฃ :
CFRunLoopStop()
- ํ์ฌ ๋ฐ๋ฃจํ ์ป๊ธฐ :
CFRunLoopGetCurrent()
var count = 0
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { t in
count += 1
print(count)
if count >= 5 {
t.invalidate()
CFRunLoopStop(CFRunLoopGetCurrent())
}
}
// run ์ ์ธ ์ด์ ์ ์ ์ธํ ํ์ด๋จธ๋ค๋ง ๋์ํ๋ค.
CFRunLoopRun()
๋ฐฉ๋ฒ2 - run(until:) ์ฌ์ฉ
RunLoop์ run ๋ฉ์๋ ์ค..
- run() ์ ๋ฌดํ๋ฐ๋ณตํ ๊ฒ ์๋๋ผ๋ฉด ์ฐ์ง ๋ง์์ผ ํ๋ค. RunLoop๋ ์ข ๋ฃ ๋ฉ์๋๋ฅผ ๋ฐ๋ก ์ ๊ณตํ์ง ์๋๋ค. ์คํ ํ ์๋์ผ๋ก ์ข ๋ฃ๋๋ ๋ฉ์๋๋ฅผ ์ด์ฉํด์ผ ํ๋ค.
- run(until:) : ์ง์ ์๊ฐ๊น์ง ์คํ. ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ.
var count = 0
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { t in
count += 1
print(count)
if count >= 5 {
t.invalidate()
}
}
while count < 5 {
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
2. DispatchQueue.main.asyncAfter(deadline:execute:)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("Timer fired!")
}
ํน์ ์์ ์ ์์ ๋ด์ฉ์ ์คํ์ ์์ฝํด ๋๊ณ , ์ฆ์ ๋ฐํํจ.
๋ฐ๋ณต์ด ์๋ ๋จ์ ์ง์ฐ ์ฝ๋๋ฅผ ์์ฑํ ๋ ์ ์ฉ.
๋น๋๊ธฐ๋ก ์คํ๋๋ฏ๋ก, asyncAfter๋ฅผ ๋ฐ๋ณต์คํํด๋ ์ง์ ์๊ฐ ํ์ ๋ฐ๋ณต๋๋ ๊ฒ ์๋๋ผ ์ฆ์ ๋ฐ๋ณต๋จ
์๋์ ๊ฐ์ด ์ฌ์ฉํด๋ 1์ด๋ง๋ค ๋ฐ๋ณต๋์ง ์๋๋ค.. ์ฃผ์
var isRunning = true var loop = 0 while isRunning { print(Thread.current.isMainThread) print("asyncafter") DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { print("asyncAfter!") } loop += 1 if loop == 10 { isRunning = false } }
Timer๋ณด๋ค ๊ฐ๋จํ๋ค.
GCD๋ฅผ ํ์ฉํ๋ค.
DispatchSource.makeTimerSource(queue:)
- ๋ฐฑ๊ทธ๋ผ์ด๋ ํ์์ ์คํ๋๋ ํ์ด๋จธ
- ๋ฐ๋ฃจํ๋ฅผ ํ์๋ก ํ์ง ์์
var timer: DispatchSourceTimer!
private func startTimer() {
let queue = DispatchQueue(label: "com.domain.app.timer")
timer = DispatchSource.makeTimerSource(queue: queue)
timer.setEventHandler { [weak self] in
// do something
}
timer.schedule(deadline: .now(), repeating: 1.0)
timer.resume()
}