KoreaMango 나무

[iOS App Dev Tutorials] SwiftUI - State Management (3) 본문

iOS/iOS App Dev Tutorials

[iOS App Dev Tutorials] SwiftUI - State Management (3)

KoreaMango 2022. 5. 18. 21:21
 

Apple Developer Documentation

 

developer.apple.com

3. Managing State and Life Cycle

Section 1. Create an Overlay View

struct MeetingView: View {
		@Binding var scrum : DailyScrum
    var body: some View {
        ZStack {
					RoundedRectangle(cornerRadius: 16.0)
					VStack{
						...
					}
				}
		}
}

ZStack를 최상단에 추가해서 앞뒤의 간격을 주었다. ZStack은 먼저 나온 View가 맨 뒤에 배치하게 된다.

따라서 RoundedRectangle이 맨 뒤에 배치되고 그 앞으로 VStack이 나열된다.

그리고 @Binding scrum을 선언하여 DetailView에서 해당 스크럼을 클릭했을 때 MeetingView로 scrum을 전달하고 그 값을 사용한다.

Section 2. Extract the Meeting Header

SwiftUI는 작은 View로 큰 View를 구성하는 것이 가능하다.

웹사이트 처럼 헤더, 본문, 푸터를 다 짤라서 컴포넌트화 시킨다. 그렇게 하면 코드를 관리하기 더욱 쉬울 것이다.

struct MeetingHeaderView: View {
    let secondsElapsed: Int
    let secondsRemaining: Int
    
    private var totalSeconds: Int {
        secondsElapsed + secondsRemaining
    }
    private var progress: Double {
        guard totalSeconds > 0 else { return 1 }
        return Double(secondsElapsed) / Double(totalSeconds)
    }
    private var minutesRemaining: Int {
        secondsRemaining / 60
    }
    var body: some View {
			...
		}
}

struct MeetingHeaderView_Previews: PreviewProvider {
    static var previews: some View {
        MeetingHeaderView(secondsElapsed: 60, secondsRemaining: 180)
            .previewLayout(.sizeThatFits)
    }
}

원래 MeetingView의 VStack에 있는 내용을 새로운 SwiftUI 파일에 옮겨서 만든 View이다.

원래 있던 MeetingView에서 값만 전달하면 그 값에 맞게 계산하고 view를 생성한다.

값을 받기 위해서 View에 변수를 생성했다. 아래에 totalSeconds, progrss, minutesRemaining 은 private이기 때문에 preview에서 값을 안주나 싶었지만! private를 지워도 preview가 에러를 생성하지 않는 것으로 보아 함수이기 때문인 것 같다. 모양이 비슷해서 자칫하면 헷갈릴 수도 있지만 함수가 아닌 변수만 매개변수로 원하는 것 같다.

Section 3. Extract the Meeting Header

struct MeetingHeaderView: View {
    ...
    var body: some View {
        VStack {
            ProgressView(value: progress)
                .progressViewStyle(ScrumProgressViewStyle(theme: theme))
            HStack {
                ..
            }
        }
        .accessibilityElement(children: .ignore)
        .accessibilityLabel("Time remaining")
        .accessibilityValue("\\(minutesRemaining) minutes")
				.padding([.top, .horizontal])
    }
}

여기서 progressViewStyle로 ScrumProgressViewStyle을 불러오는데 튜토리얼에서는 ScrumProgressViewStyle 을 만드는 방법에 대해 설명해주지 않고 첨부된 프로젝트에서 들고와라고 한다.

따라서 제일 상단에 첨부되어 있는 프로젝트를 설치해서 ScrumProgressViewStyle 을 내 프로젝트에 넣었다.

마지막에 .padding([.top, .horizontal]) 을 보면 .padding() 안에 Edge.Set 이라는 매개변수가 들어간다. 저렇게 값을 넣지 않으면 default 값으로 16이 들어간다.

Edge.Set 매개변수에는 top, bottom, leading, trailing, horizontal, vertical이 있다.

Section 4. Add a Status Object for a Source of Truth

이 부분도 첨부된 프로젝트에서 ScrumTimer을 내 프로젝트에 옮겨야한다.

이런 부분은 튜토리얼에서 구현하기 아직 이르다고 생각한 것 같다. 이 ScrumTimer 는 정기적으로 회의 상태를 업데이트 한다.

struct MeetingView: View {
    @Binding var scrum: DailyScrum
    @StateObject var scrumTimer = ScrumTimer()
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 16.0)
                .fill(scrum.theme.mainColor)
            VStack {
                MeetingHeaderView(secondsElapsed: scrumTimer.secondsElapsed, secondsRemaining: scrumTimer.secondsRemaining, theme: scrum.theme)
                ...
                }
            }
        }
        .padding()
        .foregroundColor(scrum.theme.accentColor)
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct MeetingView_Previews: PreviewProvider {
    static var previews: some View {
        MeetingView(scrum: .constant(DailyScrum.sampleData[0]))
    }
}

ScrumTimer 는 Class이기 때문에 @StateObject로 선언해주었다. 진실 소스를 MeetingView에서 생성하고 하위 자식 view에 scrumTimer 의 인스턴스를 넘겨줄 것이다.

아까 VStack에서 옮긴 코드로 만든 MeetingHeaderView를 옮긴 코드 자리에 넣어준다.

Section 5. Add Life Cycle

이 섹션에서는 onAppear, onDisAppear Life Cycle을 추가해볼 것이다.

.onAppear {
  scrumTimer.reset(lengthInMinutes: scrum.lengthInMinutes, attendees: scrum.attendees)
  scrumTimer.startScrum()
}
.onDisappear {
  scrumTimer.stopScrum()
}

onAppear, 즉 view가 나타났을 때는scrumTimer Class 안에 있는 메소드를 사용해서 스크럼을 초기화, 시작한다.

onDisappear, view가 사라질 때는 Scrum을 멈출 것이다.

Section 6. Extract the Meeting Footer

Header와 마찬가지로 Footer도 비슷하게 만들어줄 것이다.

struct MeetingFooterView: View {
    let speakers: [ScrumTimer.Speaker]
    var skipAction: ()->Void
    
    private var speakerNumber: Int? {
        guard let index = speakers.firstIndex(where: { !$0.isCompleted }) else { return nil}
        return index + 1
    }
    private var isLastSpeaker: Bool {
        return speakers.dropLast().allSatisfy { $0.isCompleted }
    }
    private var speakerText: String {
        guard let speakerNumber = speakerNumber else { return "No more speakers" }
        return "Speaker \\(speakerNumber) of \\(speakers.count)"
    }
    var body: some View {
        VStack {
            HStack {
                if isLastSpeaker {
                    Text("Last Speaker")
                } else {
                    Text(speakerText)
                    Spacer()
                    Button(action: skipAction) {
                        Image(systemName:"forward.fill")
                    }
                    .accessibilityLabel("Next speaker")
                }
            }
        }
    }
}
struct MeetingFooterView_Previews: PreviewProvider {
    static var previews: some View {
        MeetingFooterView(speakers: DailyScrum.sampleData[0].attendees.speakers, skipAction: {})
            .previewLayout(.sizeThatFits)
    }
}

클로져를 이용해 변수에다가 함수를 넣어서 Text , Button의 Action에 사용했다. 이것을 보고 자세하게 공부해야할 내용이 생긴 것 같다.

  1. 변수에다가 넣는 함수
  2. 클로져
  3. 일급 객체

Section 7. Triger Sound with AVFoundation

AVFoundation 을 사용하면 애플 플랫폼에서 오디오를 넣을 수 있다.

AVFoundation을 어떻게 사용하나 알아보자.

import AVFoundation

우선 사용할 파일에 AVFoundation을 import를 해야한다.

private var player: AVPlayer { AVPlayer.sharedDingPlayer }

그리고 player 변수에다가 AVPlayer를 이용해 음악 파일에 대한 정보를 넣는다.

( 음악 파일에 대한 정보를 넣는 방법은 다양하니까 간단하게 설명만 하는 것 같다.)

.onAppear {
		scrumTimer.reset(lengthInMinutes: scrum.lengthInMinutes, attendees: scrum.attendees)
		scrumTimer.speakerChangedAction = {
    player.seek(to: .zero)
    player.play()
}

그리고 원하는 자리에 player 변수를 play 해주면 된다.