스위프트/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와 기본 데이터의 동기화를 쉽게 유지할 수 있다.