티스토리 뷰
프로젝트에서 Timer를 사용하는데 Timer가 중복으로 실행되어 실행횟수만큼 초가 플러스 되는 문제가 생겼다.
그 문제는 @State의 잘못된 사용 때문이었다,, 간단한 예시를 들어 설명해보겠다.
✏️ @State란??
@State는 SwiftUI에서 사용되는 속성 래퍼로, 값의 변경을 감지고 뷰를 자동으로 업데이트하는 데 사용된다.
@State 속성은 주로 View 구조체 내에서 사용된다고 한다. 왜 그런지는 아래에서 설명하겠다.
1️⃣ View의 내부에서 @State 사용
View 내부에서 @State를 사용하여 currentTimer, isTimerRunning 변수, timer를 선언해보자.
@State를 사용할 때 주의할 점으로는 항상 private로 선언하고 가장 상위 뷰에서 @State를 관리해야한다고 나와있다.
- 뷰를 초기화할 때, @State 속성 값도 같이 초기화하게 되면 SwiftUI에서 @State 속성을 관리하는 공간인 Storage에서 conflict가 나기 때문이라고 한다.
예제에서는 ContentView 하나만 존재하니 private만 신경써서 선언하였다.
import SwiftUI
struct ContentView: View {
@State private var currentTime = 0
@State private var isTimerRunning = false // 타이머 실행 여부를 저장하는 상태 변수
@State private var timer: Timer? // 타이머 인스턴스를 저장하는 상태 변수
var body: some View {
VStack {
Text("\(currentTime)")
Button(action: {
if self.isTimerRunning {
self.stopTimer()
} else {
self.startTimer()
}
}) {
Text(self.isTimerRunning ? "타이머 멈추기" : "타이머 시작하기")
}
.padding()
}
}
// 타이머 시작 함수
private func startTimer() {
self.isTimerRunning = true
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.currentTime += 1
}
}
// 타이머 멈추기 함수
private func stopTimer() {
self.isTimerRunning = false
timer?.invalidate()
timer = nil
currentTime=0
}
}
위의 코드인 경우 @State 속성으로 선언한 변수들이 변경되면 뷰를 자동으로 업데이트한다.
- timer 시작하는 startTimer()가 실행되면 @State 속성으로 선언한 currentTime의 값이 변경되어 뷰를 자동으로 업데이트하고 변경된 값을 화면에 보여주는 것이다.
- timer 멈추고 초기화하는 stopTimer()가 실행되면 @State 속성으로 선언한 currentTime과 isTimerRunning의 값이 변경되어 타이머를 해제하고, 시간을 초기화한다.
실행해보면 문제없이 잘 실행되는 것을 확인할 수 있다.
2️⃣ ViewModel에서 @State 사용
timer 동작 로직을 ViewModel로 빼서 구현하였고 똑같이 @State 속성으로 timer를 선언하였다.
1️⃣과 달라진 점이라고는 ViewModel로 뺀 것 밖에 없다.
하지만 결과는 아래와 같이 타이머가 1초씩 증가하는게 아니라 실행 횟수만큼 증가되는 문제가 있었다.
왜 이런 문제가 발생한 걸까,,,,,
@State는 SwiftUI 뷰 내부 상태를 나타내는 데 사용되는데, ViewModel은 뷰의 외부에 존재하는 독립적인 객체이기 때문이다.
뷰의 내부 상태가 아니기 때문에 timer는 @State 속성으로 선언되면 안된다고 한다 ㅎㅎ
그래서 timer의 @State 속성 때문에 startTimer()가 실행될 때마다 뷰가 업데이트되어 timer가 새로 만들어졌던 것이었고, 함수 실행횟수만큼 타이머의 초 카운트도 증가하게 되었다.
- ViewModel
import SwiftUI
class TimerViewModel: ObservableObject {
@Published var currentTime = 0
@Published var isTimerRunning = false
@State private var timer: Timer?//@State 사용 x
func startTimer() {
isTimerRunning = true
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.currentTime += 1
}
}
func stopTimer() {
isTimerRunning = false
timer?.invalidate()
timer = nil
currentTime = 0
}
}
- View
struct ContentView: View {
@ObservedObject var viewModel = TimerViewModel()
var body: some View {
VStack {
Text("\(viewModel.currentTime)")
Button(action: {
if self.viewModel.isTimerRunning {
self.viewModel.stopTimer()
} else {
self.viewModel.startTimer()
}
}) {
Text(self.viewModel.isTimerRunning ? "타이머 멈추기" : "타이머 시작하기")
}
.padding()
}
}
}
@State private var timer: Timer?를 private var timer: Timer?로 바꿔주니 문제가 해결되었다.
이렇게 하면 뷰의 업데이트와는 독립적으로 타이머를 관리할 수 있고, 타이머를 다룰 때 중복 실행을 방지하고 이전 타이머를 제대로 해제할 수 있다.
'스위프트 > SwiftUI' 카테고리의 다른 글
[SwiftUI] - Unit Testing (0) | 2024.05.13 |
---|---|
[SwiftUI] - Kakao ID 토큰에 nonce 추가 (0) | 2024.04.25 |
[SwiftUI] - NavigationLink Action 추가 (0) | 2024.04.07 |
[SwiftUI] - Custom Font 적용 (0) | 2024.03.19 |
[SwiftUI] - ObservableObject 예제 (0) | 2024.03.12 |