SwiftUI + Combine을 이용하여 Fleek Copy app 만들기(3-2)

이전 포스트에 이어, 오늘은 첫시간에 만든 IntroView가 나타난 후, 지난시간에 만든 탭뷰가 노출되도록 하는 작업을 진행해볼 예정입니다.

최초 introView가 노출되고, 로딩바가 꽉 차면, 인트로화면에서 메인탭 화면으로 이동하도록 하는것이 목표입니다.


그러기 위해서는 @State라는 키워드를 알아야 합니다.

스크린샷 2023-03-23 오후 10.17.38.png

  • Apple Document에서 소개하는 대로 해석해보자면, 어떠한 값을 읽고 쓸수있는 property wrapper 타입입니다.
  • 무슨말인지 잘 이해가 가지 않는다면, 역시 코드를 봐야겠습니다.
    • isPlaying이라는 @State 키워드의 Bool 변수가 있습니다.
    • Button은 isPlaying의 값에 따라 “Pause” : “Play” 라는 Label을 노출합니다.
    • Button이 눌릴때마다, isPlaying,toggle()을 통해 true ↔ false로 변경됩니다.
    • Button의 label은 그럼 어떻게 될까요?

      Simulator Screen Recording - iPhone 14 Pro - 2023-03-23 at 22.28.35.gif

      • 최초 isPlaying의 값은 false였기 떄문에, 최초 실행시에는 button의 label은 play로 노출됩니다.
      • button을 누르게 되면 isPlaying 의 bool 값이 toggle되면서 play ↔ pause로 변경됩니다.
    • @State로 선언된 프로퍼티는 변경될 때 마다, 해당 프로퍼티를 감시하고 있는 view들을 갱신해줍니다.
    • 값이 변경될 떄마다 button이 갱신되며 label이 변경되는것입니다.

해당 키워드를 사용하여 저희는 isLoading이라는 변수를 선언하고,

progress가 100%가 되면 isLoading 변수를 toggle 해주는 방식으로 노출되어야 할 뷰를 갱신해보도록 하겠습니다.

import SwiftUI
import Combine

struct SplashView: View  {
    @State private var progress: Float = 0.0
    @State private var isComplete: Bool = false
    
    let duration: Double = 3.0
    
    var timer: Publishers.Autoconnect<Timer.TimerPublisher> {
        Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
    }
    
    var body: some View {
        GeometryReader { geo  in
            Group {
                if !isComplete {
                    VStack {
                        Text("FLEEK")
                            .foregroundColor(.red)
                            .fontWeight(.heavy)
                            .font(.largeTitle)
                        Text("당신의 운동을 의미있게!")
                            .foregroundColor(.white)
                       ProgressView(value: progress)
                            .animation(.linear(duration: duration), value: "")
                            .tint(.red)
                            .frame(width: 160)
                    }
                    .frame(minWidth: geo.size.width, maxWidth: .infinity, maxHeight: .infinity)
                    .background(.black)
                } else {
                    MainTabView()
                }
            }
            .onReceive(timer, perform: { _ in
                if progress >= 1.0 {
                    isComplete = true
                } else {
                    progress = min(1.0,progress + 0.004)
                }
            })
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        SplashView()
    }
}
  • State 키워드를 사용하여 progress와 isComplete 두개의 변수를 선언합니다.
    • progress는 ProgressView의 진행률을 업데이트 하기 위한 변수이며
    • isComplete는 ProgressView의 process가 완료되면 메인뷰를 보여주기 위한 변수입니다.
  • 자동으로 프로그레스가 진행되도록 하기위해 Timer를 생성해 주었습니다.
    • 매 0.01초마다 메인쓰레드의 런루프를 통해 호출되는 타이머를 생성합니다.
    • onReceive 메소드는 Publisher가 방출한 Data를 감지합니다.
    • 0.01초마다 타이머가 호출되며, 호출될 떄마다 onReceive 메소드의 클로져가 호출되며 progress(진행상태)를 업데이트 해줍니다.
    • 1.0이 넘어가면 isComplete 값을 true로 변환해줍니다.

  • 예상되는 동작은 다음과 같습니다.
    • 최초 progress는 0이고, isComplete는 false이므로 introView가 보입니다.
    • timer가 호출되고 progress가 1이 될 떄까지, progress에 0.004초씩 더해줍니다.
    • progress 변수의 값이 변할때 마다 ProgressView는 @State 에 반응하여 변경되는 progress의 값만큼 진행도를 업데이트 해줍니다.
    • progress가 1.0이 되면 isComplete의 값을 true로 설정합니다.
    • isComplete가 true가 되면 뷰가 갱신되며, else { } 구문이 실행되어 mainView가 노출됩니다.

    실제로 동작하는지 확인해 보도록 하겠습니다.

    Simulator Screen Recording - iPhone 14 Pro - 2023-03-23 at 22.49.12.gif

    원하는대로 동작하는 것을 확인할 수 있습니다.

    근데 뭔가 인트로(런치스크린) 화면이 노출되고 바로 메인이 나오는게, 어색합니다.

    보통은 인트로화면이 splash 되면서 사라지고 메인화면이 나오는게 자연스러운데요.

    다음 시간에는 splash되는 intro화면을 만들어 보도록 하겠습니다.