스위프트/SwiftUI
[Swift] SwiftUI StopWatch 구현
강철곰탱이
2024. 1. 22. 17:48
StopWatch
📍기능
- 재생 버튼 클릭시 stopwatch 실행
- 일시정지 버튼 클릭시 stopwatch 정지
- Reset 버튼 클릭시 stopwatch 초기화
1️⃣ SwiftUI 프로젝트 만들기
2️⃣ UI 구현
다음과 같은 코드로 구현할 수 있었다.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
HStack {
Spacer() // 오른쪽 정렬
Button("Reset") {
// 타이머 리셋
}
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: 20)
Text(String(format: "%.1f", currentTimer))
.font(.system(size: 90))
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: 250)
Spacer()
HStack(spacing: 0) {
Button {
// 타이머 재생
} label: {
Image(systemName: "play.fill")
.resizable() // 이미지 크기 조절
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
Button {
// 타이머 정지
} label: {
Image(systemName: "pause")
.resizable()
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
}
Spacer() // 동적으로 조절
}
.background(Color.black)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
🏷 SwiftUI
- Label: 문자열과 icon을 같이 표시
- Text: 문자열 표시
🏷 UIKit
- UILabel: 문자열 표시
즉 UIKit에서 사용하던 UILabel은 SwiftUI의 Text와 같은 역할이지만, Text가 좀 더 광범위한 표현이 가능하다.
Text는 단순한 문자열 표현뿐만 아니라 Button, Toggle등과 같은 UI들을 구성할 때 Text를 적용할 때 사용한다.
더보기
✏️ Label 사용법
var body: SomeView {
Label("text 값", systemImage: "some image")
}
✏️ Text 사용법
var body: SomeView {
Text("text 값")
}
3️⃣ Timer 기능 적용
- timer 객체
- currentTimer 타이머 현재 값
▶️ 타이머 재생 버튼 클릭시
- currentTimer를 0.1초씩 증가시킨다.
Button {
// 타이머 재생
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
currentTimer += 0.1
}
}
▶️ 타이머 정지 버튼 클릭시
- 타이머 정지
Button {
// 타이머 정지
timer.invalidate()
}
▶️ 타이머 Reset 버튼 클릭시
- 타이머 정지
- currentTimer 초기화
Button("Reset") {
// 타이머 리셋
timer.invalidate()
currentTimer = 0.0
}
- 전체코드 -
- 오류코드
import SwiftUI
struct ContentView: View {
@State private var currentTimer: Float = 0.0 // 타이머 현재 값
@State private var timer: Timer = Timer()//타이머 객체
var body: some View {
VStack {
HStack {
Spacer() // 오른쪽 정렬
Button("Reset") {
// 타이머 리셋
timer.invalidate()
currentTimer = 0.0
}
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: 20)
Text(String(format: "%.1f", currentTimer))
.font(.system(size: 90))
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: 250)
Spacer()
HStack(spacing: 0) {
Button {
// 타이머 재생
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
currentTimer += 0.1
}
} label: {
Image(systemName: "play.fill")
.resizable() // 이미지 크기 조절
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
Button {
// 타이머 정지
timer.invalidate()
} label: {
Image(systemName: "pause")
.resizable()
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
}
}
.background(Color.black)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
위의 코드로 실행해보면 다음과 같은 오류가 나온다.
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
위의 오류는 적절하게 초기화되지 않은 UI요소나 속성에 액세스하려고 할 때 일반적으로 발생한다고 한다.
timer가 초기화되지 않은 상태에서 실행하려고 하니 오류가 발생했던 것 같다.
timer를 다음과 같이 초기화 시켜주니 오류가 해결되었다.
@State private var timer: Timer? = nil
- 성공 코드
import SwiftUI
struct ContentView: View {
@State private var currentTimer: Float = 0.0
@State private var timer: Timer? = nil
var body: some View {
VStack{
HStack {
Spacer()//오른쪽 정렬
Button("Reset") {
//타이머 리셋
timer?.invalidate()
timer = nil
currentTimer = 0.0
}.font(.system(size: 20,weight: .medium)).foregroundColor(.white)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: 20)
Text(String(format: "%.1f", currentTimer))
.font(.system(size: 90)).foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: 250)
Spacer()
HStack(spacing: 0) {
Button{
//타이머 재생
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
currentTimer += 0.1
}
}
} label: {
Image(systemName: "play.fill")
.resizable()//이미지 크기 조절
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
Button{
//타이머 정지
timer?.invalidate()
timer = nil
} label: {
Image(systemName: "pause")
.resizable()
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
}
}
.background(Color.black)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
=> 실행하면 정상적으로 Timer가 작동하는 걸 확인할 수 있다!!
4️⃣ MVVM 적용
MVVM은 view, viewModel, Model로 이루어져 있다고 했다.
MVVM 패턴을 적용하여 코드를 정리해보자.
[swift] - MVVM 패턴
MVC 패턴에 대해 알아본 후, View와 Controller의 의존성이 높은 문제를 알게되었고 이를 해결하기 위한 MVVM패턴에 대해 정리해보고자 한다. [swift] MVC 패턴 swift uikit의 대표적인 패턴인 MVC 패턴에 대해
steelbeartaeng2.tistory.com
📍 동작과정
: 버튼을 클릭하면 ViewModel은 currentTimer를 업데이트하고, view는 자동으로 업데이트된다.
- ViewModel
import SwiftUI
class TimerViewModel: ObservableObject {
@Published var currentTimer: Float = 0.0
private var timer: Timer? = nil
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
self.currentTimer += 0.1
}
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
func resetTimer() {
stopTimer()
currentTimer = 0.0
}
}
- View
import SwiftUI
struct TimerView: View {
@ObservedObject private var viewModel = TimerViewModel()
var body: some View {
VStack {
HStack {
Spacer()
Button("Reset") {
viewModel.resetTimer()
}
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: 20)
Text(String(format: "%.1f", viewModel.currentTimer))
.font(.system(size: 90))
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: 250)
Spacer()
HStack(spacing: 0) {
Button {
viewModel.startTimer()
} label: {
Image(systemName: "play.fill")
.resizable()
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
Button {
viewModel.stopTimer()
} label: {
Image(systemName: "pause")
.resizable()
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
}
Spacer()
}
.background(Color.black)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
TimerView()
}
}
1️⃣ @ObservedObject
: view와 관찰가능한 객체 사이에 연결을 설정하여 객체에 변경사항이 있을 때마다 view에 알린다.
2️⃣ @Published
: 속성 값이 변경될 때마다 변경사항을 관찰자(view)에게 자동으로 전파한다.
@ObservedObject와 @Published를 사용하는 이유?
: ObservedObject의Published 속성이 변경될 때마다 UI를 자동으로 업데이트할 수 있어, UI와 기본 데이터의 동기화를 쉽게 유지할 수 있다.