특정 시간이 경과한 후에 실행되는 타이머는 대상 객체에 지정된 메시지를 보냅니다
타이머는 실행 루프와 함께 작동합니다. 실행 루프는 타이머에 대한 강력한 참조를 유지하므로, 따로 실행 루프에 추가한 후 타이머에 대한 강력한 참조를 유지할 필요가 없습니다.
타이머를 효율적으로 사용하기 위해서는 실행 루프가 어떻게 동작하는지를 알고 있어야 합니다. 스레딩 프로그래밍 가이드
타이머는 실시간 메커니즘이 아닙니다. 타이머의 발사 시간이 긴 실행 루프 콜아웃 중에 발생하거나 실행 루프가 타이머를 모니터링하지 않는 모드에 있는 동안 발생하는 경우, 타이머는 다음에 실행 루프가 타이머를 확인할 때까지 실행되지 않습니다. 그에 따라서 타이머가 작동하게 되는 시간은 실제 시간과 일치하지 않을 수 있습니다. 더 알고 싶으면 타이머 오차범위를 확인합니다.
반복하는 타이머와 반복하지 않는 타이머 비교하기
생성 시간에 타이머가 반복되는지 아니면 반복되지 않는지 지정합니다. 반복되지 않는 타이머는 한 번 시작된 다음 자동으로 무효화되어 타이머가 다시 시작되는 것을 방지합니다. 대조적으로, 반복 타이머가 발생한 다음 동일한 실행 루프에서 일정을 변경합니다. 반복 타이머는 항상 실제 시작 시간과 달리 예정된 시작 시간에 따라 일정을 잡습니다. 예를 들어, 타이머가 특정 시간과 그 후 5초마다 시작될 예정인 경우, 실제 시작 시간이 지연되더라도 예정된 시작 시간은 항상 원래의 5초 간격으로 떨어집니다. 시작 시간이 예정된 시작 시간 중 하나 이상을 통과할 정도로 지연되면, 타이머는 그 기간 동안 한 번만 시작됩니다. 타이머는 시작 후 향후 다음 예정된 시작 시간을 위해 일정이 변경됩니다.
타이머 허용 오차
iOS 7 이후로는 타이머의 허용 오차를 정할 수 있게 되었습니다. 타이머가 시작될 때의 이러한 유연성은 전력 절약과 반응성을 높이기 위해 시스템의 최적화 능력을 향상시킵니다. 타이머는 예정된 시작 날짜와 예정된 시작 날짜 + 허용 오차 사이에 언제든지 시작될 수 있습니다. 타이머는 예정된 시작 날짜 이전에 시작되지 않습니다. 반복 타이머의 경우, 다음 시작 날짜는 표류(drift)를 피하기 위해 개별 시작 시간에 적용되는 허용 오차에 관계없이 원래 시작 날짜에서 계산됩니다. 기본값은 0이며, 이는 추가 허용 오차가 적용되지 않는다는 것을 의미합니다. 이 시스템은 허용 오차에 대한 최대 값을 시행할 수 있습니다.
실행 루프에서 타이머 스케줄링하기
실행 루프에서는 하나의 타이머만 동록할 수 있습니다. 또한 실행 루프 안에 여러개의 루프 모드를 추가할 수 있습니다.
- scheduledTimer(timeInterval:invocation:repeats:) 또는 scheduledTimer(timeInterval:target:selector:userInfo:repeats:) 클래스 메서드를 사용하여 타이머를 만들고 기본 모드에서 현재 실행 루프에 예약합니다.
- init(timeInterval:invocation:repeats:) 또는 init(timeInterval:target:selector:userInfo:repeats:) 클래스 메서드를 사용하여 실행 루프에서 예약하지 않고 타이머 객체를 생성합니다. (만든 후, 해당 실행 루프 객체의 add(_:forMode:) 메서드를 호출하여 수동으로 실행 루프에 타이머를 추가해야 합니다.)
- init(fireAt:interval:target:selector:userInfo:repeats:) 메서드를 사용하여 타이머를 할당하고 초기화합니다. 만든 이후 add(_: forMode:) 메서드를 사용하여 타이머를 실행 루프에 직접 추가해야합니다
실행 루프에서 예약되면, 타이머는 무효화될 때까지 지정된 간격으로 실행됩니다. 반복되지 않는 타이머는 실행 직후에 무효화됩니다. 그러나, 반복 타이머의 경우, invalidate() 메서드를 호출하여 타이머 객체를 직접 무효화해야 합니다. 이 메서드를 호출하면 현재 실행 루프에서 타이머를 제거해야 합니다. 결과적으로 타이머가 설치된 동일한 스레드에서 항상 invalidate() 메서드를 호출해야 합니다. 타이머를 무효화하면 즉시 비활성화되어 더 이상 실행 루프에 영향을 미치지 않습니다. 그런 다음 실행 루프는 invalidate() 메서드가 반환되기 직전 또는 나중에 타이머(그리고 타이머에 대한 강력한 참조)를 제거합니다. 일단 무효화되면, 타이머 객체는 재사용할 수 없습니다.
반복 타이머가 시작된 후, 지정된 허용 오차 내에서 마지막으로 예정된 시작 날짜 이후 타이머 간격의 정수 배수인 가장 가까운 미래 날짜에 대한 다음 시작를 예약합니다. 선택기 또는 호출을 수행하는 데 걸리는 시간이 지정된 간격보다 긴 경우, 타이머는 다음 발사만 예약합니다. 즉, 타이머는 지정된 선택기 또는 호출을 호출하는 동안 발생한 누락된 시작을 매꾸려고 않습니다.
타이머 만들기
TimePublisher
다음과 같이 Timer 클래스의 publish 메서드를 만들게 되면 TimerPublisher가 반환되어 timer에 삽입됩니다.
autoconnect() 메서드를 사용하면 publisher와 연결하거나 연결을 끊는 프로세스를 자동화 할 수 있도록 해줍니다.
let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
every (interval)
: 이벤트가 언제 publish되는지 결정하는 TimerInterval 입니다.
: 1.0 으로 설정하면 1초마다 이벤트를 발생시킵니다.
tolerance
: 허용 오차입니다.
on (runLoop)
: 타이머가 실행되는 실행 루프를 의미하며 .main으로 설정하면 메인 스레드에서 작업하게 됩니다.
in (mode)
: 어떤 방식으로 타이머 실행 루프를 실행할 것인지를 결정합니다. common, default, tracking 이 있습니다.
struct ContentView: View {
let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
@State var currentDate: Date = Date()
// Current Time
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .medium
return formatter
}
var body: some View {
ZStack {
RadialGradient(gradient: Gradient(colors: [Color(.purple), Color.accentColor]), center: .center, startRadius: 5, endRadius: 500)
.ignoresSafeArea()
Text(dateFormatter.string(from: currentDate))
.font(.system(size: 100, weight: .semibold, design: .rounded))
.foregroundStyle(.white)
.lineLimit(1)
.minimumScaleFactor(0.1) // 너무 크면 자동으로 크기 조절해줍니다
}
.onReceive(timer, perform: { value in
// TimerPublisher는 date를 반환합니다
currentDate = value
})
}
}
10부터 0까지 카운트 다운 만들기
다음 코드에서는 TimerPublisher 가 1초마다 이벤트를 발생시키므로 onReceive 가 1초마다 실행되는 점을 이용하여 count 변수를 만들어 한번 onReceive 가 실행될때마다 1씩 감소하고 감소한 값이 화면에 뜨도록 합니다.
struct ContentView: View {
let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
// Countdown
@State var count: Int = 10
@State var finishedText: String? = nil
var body: some View {
ZStack {
RadialGradient(gradient: Gradient(colors: [Color(.purple), Color.accentColor]), center: .center, startRadius: 5, endRadius: 500)
.ignoresSafeArea()
Text(finishedText ?? "\(count)")
.font(.system(size: 100, weight: .semibold, design: .rounded))
.foregroundStyle(.white)
.lineLimit(1)
.minimumScaleFactor(0.1) // 너무 크면 자동으로 크기 조절
}
.onReceive(timer, perform: { value in
if count <= 1 {
finishedText = "종료"
} else {
count -= 1
}
})
}
}
24시간 타이머 만들기
다음은 Date 타입을 이용하여 남아있는 시간을 감소시키는 코드입니다. Calendar.current.date 를 사용하여 byAdding 에 .day를 사용하여 현재 시간에서 시간을 더한 값을 저장하는 값입니다.
Calendar.current.dateComponents 를 사용하여 dateComponents를 이용하여 나타낼 시간을 구성해줍니다. 이후 구성된 형식으로 Text를 사용하여 화면에 띄워줍니다.
struct ContentView: View {
let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
@State var timeRemaining: String = ""
// 앱을 만들 때는 얼만큼 시간이 남았는지를 설정하는 부분입니다.
let futureDate: Date = Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date()
func updateTimeRemaining() {
let remaining = Calendar.current.dateComponents([.hour, .minute, .second], from: Date(), to: futureDate)
let hour = remaining.hour ?? 0
let minute = remaining.minute ?? 0
let second = remaining.second ?? 0
timeRemaining = "\(hour):\(minute):\(second)"
}
var body: some View {
ZStack {
RadialGradient(gradient: Gradient(colors: [Color(.purple), Color.accentColor]), center: .center, startRadius: 5, endRadius: 500)
.ignoresSafeArea()
Text(timeRemaining)
.font(.system(size: 100, weight: .semibold, design: .rounded))
.foregroundStyle(.white)
.lineLimit(1)
.minimumScaleFactor(0.1) // 너무 크면 자동으로 크기 조절
}
.onReceive(timer, perform: { value in
updateTimeRemaining()
})
}
}
애니메이션을 활용하여 타이머 만들기
다음은 애니메이션을 활용하여 1초기 지날때마다 움직이는 화면으로 시간이 지남을 보여줄 수 있습니다.
struct ContentView: View {
let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
@State var count: Int = 0
var body: some View {
ZStack {
RadialGradient(gradient: Gradient(colors: [Color(.purple), Color.accentColor]), center: .center, startRadius: 5, endRadius: 500)
.ignoresSafeArea()
TabView(selection: $count,
content: {
Rectangle()
.foregroundStyle(.red)
.tag(1)
Rectangle()
.foregroundStyle(.blue)
.tag(2)
Rectangle()
.foregroundStyle(.green)
.tag(3)
Rectangle()
.foregroundStyle(.orange)
.tag(4)
Rectangle()
.foregroundStyle(.pink)
.tag(5)
})
.frame(height: 200)
.tabViewStyle(PageTabViewStyle())
HStack(spacing: 15) {
Circle()
.offset(y: count == 1 ? -20 : 0)
Circle()
.offset(y: count == 2 ? -20 : 0)
Circle()
.offset(y: count == 3 ? -20 : 0)
Circle()
.offset(y: count == 4 ? -20 : 0)
Circle()
.offset(y: count == 5 ? -20 : 0)
}
.frame(width: 200)
.foregroundStyle(.white)
}
.onReceive(timer, perform: { value in
withAnimation(.easeInOut(duration: 0.5)) {
count = count == 5 ? 0 : count + 1
}
})
}
}
'SwiftUI' 카테고리의 다른 글
Starting Vapor (Swift로 서버 만들기) (0) | 2024.04.09 |
---|---|
타이머를 이용하여 아두이노 LED 켜기 (0) | 2024.04.09 |
JSON Data (0) | 2024.04.08 |
Typelias (0) | 2024.04.08 |
Weak Self (0) | 2024.04.07 |