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

안녕하세요.

오늘은 이전 포스트에 말했던 것 처럼, Intro화면이 진행된 후 메인화면이 노출될 때, 자연스러운 splash 애니메이션으로 전환되도록 하는 것이 목표입니다.


간단한 idea를 떠올려 보면, isComplete가 되었을 때, 인트로의 alpha는 서서히 줄어들고 mainTabView의 alpha는 서서히 증가하면서 노출되면 될 것 같습니다.

바로 코드로 적용해보겠습니다.

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 {
        Group {
            if !isComplete {
                VStack {
                    Text("FLEEK")
                        .foregroundColor(.red)
                        .fontWeight(.heavy)
                        .font(.largeTitle)
                    Text("당신의 운동을 의미있게!")
                        .foregroundColor(.white)
                    
                    ProgressView(value: progress)
                        .animation(.linear(duration: duration), value: progress)
                        .tint(.red)
                        .frame(width: 160)
                }
                
                .opacity(isComplete ? 0 : 1)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.black)
            } else {
                MainTabView()
                    .opacity(isComplete ? 1 : 0)
            }
        }
        .onReceive(timer) { _ in
            if progress < 1.0 {
                progress = min(1.0, progress + 0.004)
            } else {
                withAnimation(.easeOut(duration: 1.0)) {
                    isComplete = true
                }
            }
        }
    }
}

opacity라는 메소드를 사용하였습니다.

해당 메소드의 설명은 아래와 같습니다.

opacity(_:)

스크린샷 2023-03-25 오전 12.21.13.png

간단명료합니다.. 뷰의 투명도를 설정합니다.

위의 코드를 다시 돌아가보면,..

  • VStack으로 둘러쌓인 IntroView의 opacity는 isComplete가 true이면 0, false이면 1로 설정됩니다.
  • MainTabView()의 opacity는 그 반대로 isComplete가 true이면 1, false이면 0으로 설정됩니다.

그리고 추가적으로 WithAnimation이라는 부분이 추가되었습니다.

withAnimation(::)

스크린샷 2023-03-25 오전 12.23.54.png

설정된 animation (linear, easein, easeOut 등..)을 처리하여 계산된 Result를 반환합니다.

  • 위에서 보자면 easeOut으로 동작하는 animation을 body closure에서 변경하는 isComplete의 처리동작을 다시처리하여 리턴해줍니다.
  • isComplete값이 변경되면 view의 갱신이 수행되는데, 해당 갱신작업의 결과물에 설정한 애니메이션이 recompute된 결과물을 반환합니다.

말로 설명이 어려우니 빌드해서 결과화면을 보도록 하겠습니다.

Simulator Screen Recording - iPhone 14 Pro - 2023-03-25 at 00.29.03.gif


뭔가 원하는 애니메이션도 동작하고, 뭔가 splash가 되는것 같습니다.

  • Group으로 묶인 introView와 mainTabView가 isComplete에 반응하여 하나는 사라지고 하나는 보여지는 형태가 문제인것 같습니다.
    • 제가 원하는건 인트로가 서서히 사라지고, 메인뷰가 서서히 나타나는 형태입니다.
  • 생각을 바꿔봅니다. isComplete에 따라 뷰가 보여지는 형태가 아닌, 두개의 뷰가 계층을 두고 노출되어있는 상태에서 앞의 뷰는 서서히 사라지고 뒤의 뷰는 서서히 나타나도록 변경해보겠습니다.

Group대신 ZStack을 사용해봅니다.

ZStack

스크린샷 2023-03-25 오전 12.33.39.png

Subview들을 zIndex를 다루던 것과 같이, 그림에서 보듯이 색종이를 한장한장 쌓아두는 것 처럼 각 View의 zIndex Hierachy를 정렬하는 뷰 입니다.

  1. Group을 ZStack으로 변경해줍니다.
  2. if !iscomplete 에 따라 노출할 뷰를 변경하는 부분을 제거합니다.
  3. ZStack에 IntroView와 mainTabView를 차례대로 삽입합니다.
  4. 위에 설명했던것 처럼 opacity를 통해서 introView는 사라지고, mainTabView는 나타나도록 해줍니다.

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 {
        ZStack {
            VStack {
                Text("FLEEK")
                    .foregroundColor(.red)
                    .fontWeight(.heavy)
                    .font(.largeTitle)
                Text("당신의 운동을 의미있게!")
                    .foregroundColor(.white)
                
                ProgressView(value: progress)
                    .animation(.linear(duration: duration), value: progress)
                    .tint(.red)
                    .frame(width: 160)
            }
            .opacity(isComplete ? 0 : 1)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(.black)
            MainTabView()
                .opacity(isComplete ? 1 : 0)
        }
        .onReceive(timer) { _ in
            if progress < 1.0 {
                progress = min(1.0, progress + 0.004)
            } else {
                withAnimation(.easeOut(duration: 1.0)) {
                    isComplete = true
                }
            }
        }
    }
}

Group 에서 ZStack으로 변경한 후 다시 실행해보도록 하겠습니다.

Simulator Screen Recording - iPhone 14 Pro - 2023-03-25 at 00.43.38.gif


그런데 갑자기 timer가 신경쓰입니다.. 😟

뷰는 갱신을 하는데, timer는?? 누가 신경써주지? timer는 계속 돌고있는거 아닐까??

print문을 추가해서 확인해보았습니다

...
.onReceive(timer) { _ in
   print("timer is Running \(Date().timeIntervalSince1970)")
     if progress < 1.0 {
        progress = min(1.0, progress + 0.004)
     } else {
        withAnimation(.easeOut(duration: 1.0)) {
           isComplete = true
           print("progress complete")
        }
 }
...
timer is Running 1679672821.315446
timer is Running 1679672821.3281121
timer is Running 1679672821.340901
progress complete
timer is Running 1679672821.353378
progress complete
timer is Running 1679672821.36378
progress complete
timer is Running 1679672821.3728452
progress complete
timer is Running 1679672821.383811
progress complete
timer is Running 1679672821.39377
progress complete
timer is Running 1679672821.404144
progress complete
timer is Running 1679672821.414058
progress complete
timer is Running 1679672821.4232578
progress complete
timer is Running 1679672821.433852
progress complete
timer is Running 1679672821.4438272
progress complete
timer is Running 1679672821.454082
progress complete
timer is Running 1679672821.4638371

뭔가 끝내고 싶었는데 찜찜함이 또 남았습니다..

Combine을 설명해야 할 부분이 생긴것 같아서 오늘의 글은 이정도로 마무리하고,

다음 글로 이어서 작성해보도록 하겠습니다.

감사합니다.