[Apple Timer App] using SwiftUI

Woozoo·2023년 1월 15일
0

개인프로젝트

목록 보기
3/12

[Apple Timer App] using SwiftUI

애플 타이머 앱을 따라 만들어보자

구현해줄 뷰는 타이머 탭!

VStack으로 전체를 감싸줘야 될 거 같고,
상단에서부터 Picker 뷰,
HStack{ 취소 , 시작(일시 정지, 재개로 바뀜) 버튼}
그리고 다시 Picker뷰!

요렇게 크게보면 3개로 구성되야함

제일 먼저 시간을 선택할 PickerView가 필요함

시간, 분, 초 텍스트는 고정이고
시간은 23/ 분은 59 / 초도 59 까지 중에 선택 가능함

번외로 Picker커스텀


Picker

enum Flavor: String, CaseIterable, Identifiable {
	case chocolate, vanila, strawberry
	var id: Self { self }
}

struct TimerPickerView: View {
    
    @State private var selectedFlavor: Flavor = .chocolate
    
    var body: some View {
        List {
            Picker("Flavor", selection: $selectedFlavor) {
                Text("Chocolate").tag(Flavor.chocolate)
                Text("Vanila").tag(Flavor.vanila)
                Text("Strawberry").tag(Flavor.strawberry)
            }
        }
    }
}
enum Flavor: String, CaseIterable, Identifiable {
	case chocolate, vanila, strawberry
	var id: Self { self }
}
struct TimerPickerView: View {
    
    @State private var selectedFlavor: Flavor = .chocolate
    
    var body: some View {
        List {
            Picker("Flavor", selection: $selectedFlavor) {
                ForEach(Flavor.allCases) { flavor in
                    Text(flavor.rawValue.capitalized)                    
                }
            }
        }
    }
}

위의 두 코드는 같은 표현임


import SwiftUI

enum Flavor: String, CaseIterable, Identifiable {
    case chocolate, vanila, strawberry
    var id: Self { self }
}
enum Topping: String, CaseIterable, Identifiable {
    case nuts, cookies, blueberries
    var id: Self { self }
}
extension Flavor {
    var suggestedTopping: Topping {
        switch self {
        case .chocolate: return .nuts
        case .vanila: return .cookies
        case .strawberry: return .blueberries
        }
    }
}

struct TimerPickerView: View {
        
    @State private var selectedFlavor: Flavor = .chocolate
    @State private var suggestedTopping: Topping = .nuts
    
    var body: some View {
        List {
            Picker("Flavor", selection: $suggestedTopping) {
                ForEach(Flavor.allCases) { flavor in
                    Text(flavor.rawValue.capitalized)
                        .tag(flavor.suggestedTopping)
                }
            }.pickerStyle(.wheel)
            HStack {
                Text("Suggested Topping")
                Spacer()
                Text(suggestedTopping.rawValue.capitalized)
                    .foregroundStyle(.secondary)
            }
        }
    }
}

Multi Picker 구현하기

struct TimerPickerView: View {
    @State var hourSelection = 0
    @State var minuteSelection = 0
    @State var secondSelection = 0
    
    var hours = [Int](0..<24)
    var minutes = [Int](0..<60)
    var seconds = [Int](0..<60)
    
    var body: some View {
       
        GeometryReader { geometry in
            HStack {
                Picker(selection: $hourSelection, label: Text("")) {
                    ForEach(0..<24) { index in
                        Text("\(hours[index]) 시간").tag(index)
                    }
                }.frame(width: geometry.size.width/3, height: 100, alignment: .center)
                
                Picker(selection: $minuteSelection, label: Text("")) {
                    ForEach(0..<60) { index in
                        Text("\(minutes[index]) 분").tag(index)
                    }
                }.frame(width: geometry.size.width/3, height: 100, alignment: .center)
                Picker(selection: $secondSelection, label: Text("")) {
                    ForEach(0..<60) { index in
                        Text("\(seconds[index]) 초").tag(index)
                    }
                }.frame(width: geometry.size.width/3, height: 100, alignment: .center)
            }.pickerStyle(.wheel)
        }
    }            
}

구글링하면서 찾은 방법 중에 하나인데
아직 원하는 느낌이 안나온다..


저 선택되는 부분이 하나의 단일 바로 구성되고,
시간 분 초 같은 경우엔 고정적으로 뙇 박혀있어야함

struct TimerPickerView: View {
    @State private var hours = 0
    @State private var minutes = 0
    @State private var seconds = 0
    var body: some View {
        HStack(spacing: -30) {
            Picker("Hours", selection: $hours) {
                ForEach(0..<24) {
                    Text("\($0)").tag($0)
                }
            }
            Text("시간")
                .font(.body)
            Picker("Minutes", selection: $minutes) {
                ForEach(0..<60) {
                    Text("\($0)").tag($0)
                }
            }
            Text("분")
                .font(.body)
            Picker("Seconds", selection: $seconds) {
                ForEach(0..<60) {
                    Text("\($0)").tag($0)
                }
            }
            Text("초")
                .font(.body)
        }
        .padding()
        .pickerStyle(.wheel)
    }
}

찾은 또 하나의 방법. 여전히 원하는 느낌은 아니다
HStack으로 개별적인 Picker들을 만들고 사이에 텍스트를 넣었는데
시간이 선택되는 바가 한 묶음으로 묶여있어야함
혹시나 HStack의 spacing을 줄여서 만들어볼 수 있지 않을까 했는데

pickerStyle(.wheel)의 selection Bar가 투명도를 가지고 있는걸 알게됨

DatePicker도 가져와보고 custom이 가능한지 살펴봤는데
이것도 한정적이다


import SwiftUI
import UIKit
struct Time {
    var hour: Int = 0
    var minute: Int = 0
    var second: Int = 0
}

struct TimerPickerView: UIViewRepresentable {
    
     var time: Time

        func makeCoordinator() -> TimerPickerView.Coordinator {
            Coordinator(self)
        }

        func makeUIView(context: Context) -> UIDatePicker {
            let datePicker = UIDatePicker()
            datePicker.datePickerMode = .countDownTimer
            datePicker.addTarget(context.coordinator, action: #selector(Coordinator.onDateChanged), for: .valueChanged)
            return datePicker
        }

        func updateUIView(_ datePicker: UIDatePicker, context: Context) {
            let date = Calendar.current.date(bySettingHour: time.hour, minute: time.minute, second: time.second, of: datePicker.date)!
            datePicker.setDate(date, animated: true)
        }

        class Coordinator: NSObject {
            var durationPicker: TimerPickerView

            init(_ durationPicker: TimerPickerView) {
                self.durationPicker = durationPicker
            }

            @objc func onDateChanged(sender: UIDatePicker) {
                print(sender.date)
                let calendar = Calendar.current
                let date = sender.date
                durationPicker.time = Time(hour: calendar.component(.hour, from: date), minute: calendar.component(.minute, from: date), second: calendar.component(.second, from: date))
            }
        }
    }

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        TimerPickerView(time: Time(hour: 0, minute: 0, second: 0))
    }
}

coordinate 패턴 사용하는 방법도 있음.
이 경우엔 UIKit을 스유에서 띄워주는 방법을 사용하는 거 같음


enum TimePeriod: CaseIterable {
    case am
    case pm
    
    var displayText: String {
        switch self {
        case .am:
            return "AM"
        case .pm:
            return "PM"
        }
    }
}

struct TimerPickerView: View {
    @State private var selectedHour: Int = 0
    @State private var selectedTimePeriod: TimePeriod = .am
    var body: some View {
        GeometryReader { geometry in
            VStack {
                HStack {
                    Spacer()
                    Picker("Selected Hour", selection: $selectedHour) {
                        ForEach(0..<24, id: \.self) { hour in
                            Text("\(hour)")
                        }
                    }
                    .pickerStyle(.wheel)
                    .frame(width: geometry.size.width/3, height: 100)
                    .clipped()
                    
                    Picker("Select Period", selection: $selectedTimePeriod) {
                        ForEach(TimePeriod.allCases, id: \.self) { timePeriod in
                            Text(timePeriod.displayText)
                        }
                    }
                    .pickerStyle(.wheel)
                    .frame(width: geometry.size.width/3, height: 100)
                    .clipped()
                    
                    Spacer()
                }
            }
        }
    }    
}

geometry reader 사용해서 구현하는 법임

활용해서

struct TimerPickerView: View {
    @State private var selectedHour: Int = 0
    @State private var selectedMinute: Int = 0
    @State private var selectedSecond: Int = 0
    var body: some View {
        GeometryReader { geometry in
            VStack {
                HStack(spacing: 0) {
                    Spacer()
                    Picker("Selected Hour", selection: $selectedHour) {
                        ForEach(0..<24, id: \.self) { hour in
                            Text("\(hour)")
                        }
                    }
                    .pickerStyle(.wheel)
                    .frame(width: geometry.size.width/5, height: 200)
                    .clipped()
                    Text("시간")
                    
                    Picker("Select Period", selection: $selectedMinute) {
                        ForEach(0..<60, id: \.self) { minute in
                            Text("\(minute)")
                        }
                    }
                    .pickerStyle(.wheel)
                    .frame(width: geometry.size.width/4, height: 200)
                    .clipped()
                    Text("분")
                    
                    
                    Picker("Select Period", selection: $selectedSecond) {
                        ForEach(0..<60, id: \.self) { minute in
                            Text("\(minute)")
                        }
                    }
                    .pickerStyle(.wheel)
                    .frame(width: geometry.size.width/4, height: 200)
                    .clipped()
                    Text("초")
                    Spacer()
                }
            }
        }
    }
}

지금까지 구현한 것들 중에는 제일 근접하게 왔다
GeometryReader를 사용해서 Picker마다에 screen의 사이즈를 이용해서 frame을 지정해줌
아직 문제가 되는건 selection Bar가 하나로 묶여있지 않은거 , column 별로 여러개가 돌아가는 중이면 제대로 작동하지 않는 현상이 있음
아마 Hashable 하지 않아서 그런 것 같다


Button추가

피커뷰 디자인을 내려놓기로함...
왜 custom이 안될까!!!

ctaButton 만들어주고, 탭 추가해줌


그래도 얼추 비슷해진거 같아서 만족 중이다

profile
우주형

0개의 댓글