원문 : https://www.raywenderlich.com/4503153-how-to-create-a-splash-screen-with-swiftui

How to Create a Splash Screen With SwiftUI

애니메이션과 SwiftUI를 사용해서 일반적인 정적인 시작(launch) 화면을 넘어서 사용자가 앱을 로딩하는 동안에 흥미를 유지하도록 하는 스플래쉬(splash) 화면을 만드는 것을 배웁니다.

멋진 스플래쉬 화면 - 앱이 동작하는데 필요한 중요한 데이터에 대해서 API 종단점(endpoints)에 미친듯이 ping을 호출하는 것처럼, 개발자들이 재미있는 애니메이션으로 열광할 기회입니다. 정적(static)이며, 애니메이션이 없는 시작 화면과는 반대인 스플래쉬 화면은, 앱에서 중요한 역할을 할 수 있습니다: 앱이 시작하는 것을 기다리는 동안에 사용자의 흥미를 유지합니다. 

이 튜토리얼은 스플래쉬가 없는 화면의 앱부터 다른 앱이 부러워 할만한 멋진 스플래쉬 화면이 있는 앱까지를 단계별로 안내할 것입니다. 무얼 기다리시나요?

주의
이 튜토리얼은 SwiftUI 애니메이션, 상태(state), 수정자(modifiers)에 익숙하다고 가정합니다. 이런 개념을 소개하는 대신에, 이 튜토리얼에서는 그것들을 사용해서 멋진 애니메이션을 복제하는 것에 중점을 둡니다. SwiftUI에 대해서 자세히 배우려면, SwiftUI: Getting Started를 확인하세요.

시작하기(Getting Started)

Download Materials

이 튜토리얼에서, Fuber이라는 앱을 확장할 것입니다. Fuber는  Segway 운전자에게 도시 환경에서 다른 위치로 수송해줄 것을 요청할 수 있는 요청할 수 있는 주문형(on-demand) 승차 공유(ride-sharing) 서비스입니다.

Faber은 빠르게 성장해서 현재 60개국 이상에서 Segway 승객들에게 서비스를 제공하고 있지만, Segway 운전자 계약에 대해 많은 정부뿐만 아니라 Segway노조의 반대에 직면해 있습니다. :]

이 튜토리얼의 상단과 하단에 있는 Download Materials를 사용해서 시작 프로젝트를 다운로드 합니다. 그리고나서 시작 프로젝트를 열고 살펴보세요.

ContentView.swift에서 볼수 있는 것처럼, 모든 앱은 현재 SplashScreen을 2초 동안 보여주고나서 페이드 인(fade in)으로 MapView을 보여줍니다.

주의
출시(production) 앱에서, 이런 반복문에 대한 종료 기준은 API 종단점과의 handshake 성공이 될 수 있으며, 계속 진행하는데 필요한 데이터를 앱에게 제공합니다.

스플래쉬 화면은 자체 모듈에 있습니다: SplashScreen.swift. 애니메이션에 ‘U'가 추가되기를 기다리는 F ber 라벨과 함께 Fuber-blue 배경이 있는 것을 알 수 있습니다.

시작 프로젝트를 빌드하고 실행합니다.

몇초 뒤에 지도 화면(Fuber의 메인 화면)으로 전환하는 별로 흥미롭지 않은 정적인 스플래쉬 화면을 보게 될것입니다.

이 튜토리얼의 나머지 부분에서는 지루한 정적인 스플래쉬 화면을 아름다운 애니메이션 화면으로 변환해서 사용자가 메인 화면이 로딩되지 않았으면 하도록 만들 것입니다. 무엇을 만들 것인지 살펴보세요.

뷰와 레이어의 구성 이해하기(Understanding the Composition of Views and Layers)

새롭게 개선된 SplashScreen은 여러개의 하위뷰로 구성되며, 모두 ZStack으로 편리하게 구성될 것입니다.

  • 더 작은 Chimes 이미지의 목록으로 구성된 그리드(grid) 배경이며, 시작 프로젝트에 제공합니다.
  • F ber 텍스트는 애니메이션된 FuberU에 대한 공간이 있습니다.
  • FuberU는 U에 대해 흰색 배경 원으로 표현합니다.
  • Rectangle는 FuberU의 가운데 사각형으로 표현합니다.
  • 또 다른 Rectangle은 FuberU의 가운데에서 바깥쪽 모서리로 가는 선을 표현합니다.
  • Spacer뷰는 ZStack의 크기가 전체 화면을 덮을수 있도록 만듭니다.

이런 뷰들을 결합해서 Fuber'U’ 애니메이션을 만듭니다.

시작 프로젝트는 Text와 Spacer 뷰를 제공합니다. 다음에 오는 섹션에서 나머지 뷰들을 추가할 것입니다.

이제 레이어를 구성하는 방법을 알고 있으므로, FuberU를 만들고 애니메이션하는 것을 시작할 수 있습니다.

원 애니메이션하기(Animating the Circle)

애니메이션 작업을 할때, 현재 구현중인 애니메이션에 집중하는 것이 가장 좋습니다. ContentView.swift를 열고 .onAppear 클로져 주석(comment)을 확인하세요. 그것은 다음과 같습니다.

.onAppear {
//DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
//  withAnimation() {
//    self.showSplash = false
//  }
//}
}

이 방법은, X 초 뒤에 MapView가 나타나더라도 스플래쉬 화면이 페이드 아웃(fading out)으로 산만해지는것을 원하지 않습니다. 걱정마세요, 동작할 준비가 끝났을때 주석 부분을 해제 할 것입니다.

이제 애니메이션에 집중할 수 있습니다. SplashScreen.swift을 열어서 시작하고, SplashScreen의 닫힌 대괄호 바로 아래에, FuberU라는 새로운 구조체를 추가하세요.

struct FuberU: Shape {
  var percent: Double
  
  // 1
  func path(in rect: CGRect) -> Path {
    let end = percent * 360
    var p = Path()

    // 2
    p.addArc(center: CGPoint(x: rect.size.width/2, y: rect.size.width/2),
             radius: rect.size.width/2,
             startAngle: Angle(degrees: 0),
             endAngle: Angle(degrees: end),
             clockwise: false)
    
    return p
  }  
  // 3
  var animatableData: Double {
    get { return percent }
    set { percent = newValue }
  }
}

다음은 이 코드로 무엇을 하는지 입니다.

  1. Shape 프로토콜에서 필요한 path(in:) 구현합니다.
  2. 패스(path)를 사용해서 0에서 시작해서 360으로 끝나는 호(arc)를 그립니다. 예를들어, 완전한 원.
  3. 추가 프로퍼티를 추가하며, SwiftUI는 모양(shape)을 애니메이션하는 방법을 알고 있습니다.

새로운 타입의 동작을 보기 위해서, 몇가지 변수들과 애니메이션을 설정 할 것이며, 몇개의 수정자(modifiers)를 body에서 사용해서 선언합니다.

이런 변수들을 SplashScreen 구조체 안에 있는 body 요소 바로 앞에 추가해서 시작합니다.

@State var percent = 0.0
let uLineWidth: CGFloat = 5

초기화되고 FuberU를 수정할때 해당 변수들을 사용할 것입니다.

그리고나서, SplashScreen의 구조체 닫는 대괄호 뒤에 다음에 오는 코드를 추가합니다.

extension SplashScreen {
  var uAnimationDuration: Double { return 1.0 }
    
  func handleAnimations() {
    runAnimationPart1()
  }

  func runAnimationPart1() {
    withAnimation(.easeIn(duration: uAnimationDuration)) {
      percent = 1
    }
  }
}

handleAnimations()는 스플래쉬 화면의 복잡한 애니메이션의 다른 모든 부분의 기초가 될 것입니다. 이것은 magic numbers를 기반하며, 나중에 취향에 정확하게 맞도록 실행하고 조정할 수 있습니다.

마지막으로, body 안쪽에, 기존 Text와 Spacer 요소들 사이에 다음에 오는 코드를 추가하세요.

FuberU(percent: percent)
 .stroke(Color.white, lineWidth: uLineWidth)
 .onAppear() {
   self.handleAnimations()
 }
 .frame(width: 45, height: 45, alignment: .center)

여기에서, 결국 Fuber'U'의 부분을 표현할, 새로운 원을 지정된 위치에 있는 스택에 추가합니다. 추가적으로, 뷰가 나타날때 handleAnimations()를 호출합니다.

앱을 빌드하고 실행합니다.

여러분은 무슨일이 일어나는지 볼수 있지만, 여러분이 예상했던것과 정확하지 않습니다. 여러분의 코드는 실제로 원을 그리지만, 한번뿐이고, 그 원의 테두리는 너무 얇습니다. 여러분은 전체 원을 채우는 것을 원합니다. 여러분은 이 문제를 제대로 수정할 것입니다.

원 애니메이션 개선하기(Improving the Circle Animation)

runAnimationPart1() 바로 뒤에 이 코드를 추가해서 시작합니다.

func restartAnimation() {
  let deadline: DispatchTime = .now() + uAnimationDuration
  DispatchQueue.main.asyncAfter(deadline: deadline) {
    self.percent = 0
    self.handleAnimations()
  }
}

그리고 이 메소드를 호출하기 위해, handleAnimations()의 끝부분에 선을 추가합니다.

restartAnimation()

이 코드는 precent을 재설정하는 동안 기다리다가 다시 호출해서 애니메이션을 반복합니다.

이제 원 애니메이션이 반복되며, 원하는데로 정확하게 보이도록 FuberU에 수정자(modifiers)를 추가할 수 있습니다. 먼저, body 앞에 새로운 변수들을 추가하세요.

@State var uScale: CGFloat = 1
let uZoomFactor: CGFloat = 1.4

이제, FuberU에서 stroke(_:lineWidth:)와 onAppear() 수정자들간에, 3개의 새로운 수정자(modifiers)를 추가하세요.

.rotationEffect(.degrees(-90))
.aspectRatio(1, contentMode: .fit)
.padding(20)

마지막으로, scaleEffect(_:anchor:)을 frame(width:height:alignment:) 바로 앞에 추가하세요.

.scaleEffect(uScale * uZoomFactor)

FuberU 선언은 이제 다음과 같습니다.

FuberU(percent: percent)
  .stroke(Color.white, lineWidth: uLineWidth)
  .rotationEffect(.degrees(-90))
  .aspectRatio(1, contentMode: .fit)
  .padding(20)
  .onAppear() {
    self.handleAnimations()
  }
  .scaleEffect(uScale * uZoomFactor)
  .frame(width: 45, height: 45, alignment: .center)

이 코드는 선을 넓게 만들었으며, 그리는 것을 위에서부터 시작하도록 회전을 추가하고 애니메이션 동안에 원이 커지도록 확대 효과를 추가했습니다.

percent를 1로 업데이트한 후에, runAnimationPart1()에 있는 애니메이션 블록 안쪽에, 다음에 오는 줄을 추가해서 이 부분을 마칩니다.

uScale = 5

이 코드를 사용해서, uScale 상태를 1에서 5로 변경합니다.

앱을 빌드하고 실행합니다.

이제 원은 예상대로 동작합니다 - 여러분의 앱은 완전한 흰색 원을 0도에서 360도로 그리면서 조금씩 커집니다.

여러분은 첫번째 그리는 주기에서만 원이 커지는 것을 알 수 있을 것입니다. 그것은 uScale이 다시 초기화되지 않기 때문입니다. 걱정 마세요. 여러분은 다음 단계의 애니메이션에서 이부분을 해결할 것입니다.

주의
FuberU 수정자를 사용해 보세요 - 여러개를 삭제해보고, 하나를 추가하고, 값을 변경해보세요. 뷰의 변경사항을 관찰해보면, 각 수정자(modifier)가 하는 일을 더 잘 이해할 수 있을 것입니다.

사각형 추가하기(Adding the Square)

이제, ‘U’ 글자를 ‘U’ 처럼 보이게 하고 그 위에 사각형이 있는 원처럼 보이지 않게 하기 위해서 선을 추가해야 합니다.

body 앞에 이전의 프로퍼티와 상태(state)를 추가하세요.

@State var lineScale: CGFloat = 1

let lineWidth:  CGFloat = 4
let lineHeight: CGFloat = 28

그리고나서 ZStack의 끝부분 Spacer 바로 앞에, Rectangle 뷰를 추가합니다.

Rectangle()
  .fill(fuberBlue)
  .scaleEffect(lineScale, anchor: .bottom)
  .frame(width: lineWidth, height: lineHeight, alignment: .center)
  .offset(x: 0, y: -22)

앱을 빌드하고 실행합니다.

이제 Fuber ‘U'에 대한 모든 요소들을 가지고 있으며, 애니메이션을 조금 더 복잡하게 만들수 있습니다. 도전할 준비가 되셨나요?

U 애니메이션 완성하기(Completing the U Animation)

여러분이 만들고자하는 'U’ 애니메이션은 3 단계가 있습니다.

  • 원이 그려질때 확대하기(zooms in).
  • 원을 사각형으로 빠르게 축소하기(zooms out).
  • 사각형이 사라지기.

기존 handleAnimations()을 확장할때 이런 3단계를 사용할 것입니다. uAnimationDuration 바로 뒤에 새로운 프로퍼티들을 추가해서 시작합니다.

var uAnimationDelay: Double { return  0.2 }
var uExitAnimationDuration: Double{ return 0.3 }
var finalAnimationDuration: Double { return 0.4 }
var minAnimationInterval: Double { return 0.1 }
var fadeAnimationDuration: Double { return 0.4 }

이런 매직 넘버(magic numbers)는 시행착오의 결과입니다. 애니메이션을 개선한다고 느끼는지 확인하거나 동작 방법을 더 쉽게 이해할수 있도록 마음껏 실행해 보세요. 

runAnimationPart1()의 끝에 uScale 바로뒤에, 한 줄을 더 추가합니다.

lineScale = 1

runAnimationPart1()의 끝부분에, 애니메이션 블록의 닫힌 대괄호 바로 뒤에 다음에 오는 코드를 추가합니다.

//TODO: Add code #1 for text here

let deadline: DispatchTime = .now() + uAnimationDuration + uAnimationDelay
DispatchQueue.main.asyncAfter(deadline: deadline) {
  withAnimation(.easeOut(duration: self.uExitAnimationDuration)) {
    self.uScale = 0
    self.lineScale = 0
  }
  withAnimation(.easeOut(duration: self.minAnimationInterval)) {
    self.squareScale = 0
  }
    
  //TODO: Add code #2 for text here
}  

여기에서, 첫번째 애니메이션이 실행된 후에 종료하는 비동기 호출을 사용합니다. 텍스트 애니메이션에 대한 자리표시자(placeholders)가 있습니다; 곧 해결할 것입니다.

이제 애니메이션의 두번째 부분입니다. runAnimationPart1()의 닫힌 대괄호 뒤에 추가하세요. 

func runAnimationPart2() {
  let deadline: DispatchTime = .now() + uAnimationDuration + 
    uAnimationDelay + minAnimationInterval
  DispatchQueue.main.asyncAfter(deadline: deadline) {
    self.squareColor = Color.white
    self.squareScale = 1
  }
}

handleAnimations()에서 runAnimationPart1() 바로뒤에 새로운 함수 호출하도록 추가하세요.

runAnimationPart2()

이제, runAnimationPart2() 뒤에 애니메이션의 세번재 부분을 추가하세요.

func runAnimationPart3() {
  DispatchQueue.main.asyncAfter(deadline: .now() + 2 * uAnimationDuration) {
  withAnimation(.easeIn(duration: self.finalAnimationDuration)) {
    //TODO: Add code #3 for text here
    self.squareColor = self.fuberBlue
  }
  }
}

이 코드에는 이 튜토리얼에서 나중에 적용할 정확한 위치를 보여주는 TODO가 포함되어 있습니다.

이제, handleAnimations()안에 runAnimationPart2() 바로 뒤에 새로운 애니메이션을 추가합니다.

runAnimationPart3()

이 단계를 완료하기 위해서, restartAnimation()을 새로운 구현으로 교체합니다.

func restartAnimation() {
    let deadline: DispatchTime = .now() + 2 * uAnimationDuration + 
      finalAnimationDuration
    DispatchQueue.main.asyncAfter(deadline: deadline) {
      self.percent = 0
      //TODO: Add code #4 for text here
      self.handleAnimations()
    }
}

특정 단계에서 정의한 magic numbers를 기반으로 애니메이션의 각 단계가 특정 시점에 시작되도록 예약했되어 있는 것을 주의하세요. 

앱을 빌드하고 실행하고나서 귀여운 모습을 지켜보세요! :]

완성된 애니메이션을 보는 경우에, 텍스트가 투명하고 작게 시작되는 것을 볼수 있으며, 페이드 인(fades in)하고나서, 스프링으로 확대하고, 마지막으로, 사라집니다. 이제 모든 것이 제자리에 둬야 할 때입니다.

텍스트 애니메이션하기(Animating the Text)

F ber 텍스트는 처음부터 시작되었지만, ‘U'와 함께 애니메이션 되지 않기 때문에 지루합니다. 이를 고치기 위해서, Text에 2개의 새로운 수정자를 추가할 것입니다. 먼저, body 앞에 2개의 새로운 상태(state)를 추가합니다.

@State var textAlpha = 0.0
@State var textScale: CGFloat = 1

이제, 해당 자리표시자(placeholder)를 실제 애니메이션으로 교체할 때입니다.

//TODO: Add code #1 for text here를 다음으로 교체합니다.

withAnimation(Animation.easeIn(duration: uAnimationDuration).delay(0.5)) {
  textAlpha = 1.0
}

두번째, //TODO: Add code #2 for text here를 다음으로 교체합니다.

withAnimation(Animation.spring()) {
  self.textScale = self.uZoomFactor
}

다음으로, //TODO: Add code #3 for text here를 다음으로 교체합니다.

self.textAlpha = 0

그리고 마지막으로, //TODO: Add code #4 for text here를 다음으로 교체합니다.

self.textScale = 1

이제, Text를 다음에 오는 것으로 교체합니다.

Text("F           BER")
  .font(.largeTitle)
  .foregroundColor(.white)
  .opacity(textAlpha)
  .offset(x: 20, y: 0)
  .scaleEffect(textScale)

앱을 빌드하고 실행합니다.

텍스트 뷰는 애니메이션이 포함된 두가지 새로운 상태(state) 변수의 변경에 대해 응답합니다. 얼마나 멋지나요? :]

이제, 남아 있는 것은 배경을 추가하는 것이고 여러분의 멋진 스플래쉬 애니메이션이 완성될 것입니다. 심호흡 한번 하고 애니메이션의 마지막 부분을 살펴봅시다.

배경 애니메이션하기(Animating the Background)

ZStack의 배경을 추가하는 것으로 시작할 것입니다. 배경이므로, 스택의 뒷쪽에 있는 view가 되야 하므로, 코드에서 먼저 표시해야 합니다. 이를 위해서, SplashScreen의 ZStack의 첫번째 요소로 Image 뷰를 추가합니다.

Image("Chimes")
  .resizable(resizingMode: .tile)
  .opacity(textAlpha)
  .scaleEffect(textScale)

전체화면으로 타일을 채우기 위해 Chimes 어셋(asset)을 사용합니다. textAlpha와 textScale를 상태(state) 변수로 사용하는 것을 주의하세요. 따라서 그 뷰(view)는 해당 상태 변수들이 변경될때마다 불투명도(opacity)와 확대비율(scale)을 변경할 것입니다. F ber 텍스트 애니메이션이 이미 변경되었으므로, 활성화하기 위해 다른 작업을 수행할 필요가 없습니다.

앱을 빌드하고 실행하고 텍스트와 함께 배경이 애니메이션되는 것을 보게 될 것입니다.

이제 Fuber'U'가 사각형으로 줄어들때, 배경을 흐리게하는 물결(ripple) 효과를 추가하는게 필요합니다. 다른 모든 뷰 아래에, 배경 뷰 바로 위에 반투명 원을 추가할 것입니다. 원은 Fuber'U'의 가운데 부터 전체 화면을 덮고 배경을 숨기기 위한 애니메이션을 할 것입니다. 충분히 쉽게 들리죠? 그렇죠?

원을 애니메이션하는데 필요한 2개의 새로운 상태(state) 변수를 추가하세요.

@State var coverCircleScale: CGFloat = 1
@State var coverCircleAlpha = 0.0

그리고나서 ZStack에 배경 이미지뷰 바로 뒤에 새로운 뷰를 추가합니다.

Circle()
  .fill(fuberBlue) 
  .frame(width: 1, height: 1, alignment: .center)
  .scaleEffect(coverCircleScale)
  .opacity(coverCircleAlpha)

이제, 애니메이션을 시작하려면 정확한 순간에 상태 변수의 값을 변경해야 합니다. 이 클로져를 runAnimationPart2()에 self.squareScle = 1 바로 아래에 추가하세요.

withAnimation(.easeOut(duration: self.fadeAnimationDuration)) {
  self.coverCircleAlpha = 1
  self.coverCircleScale = 1000
}

마지막으로, 애니메이션을 완료하고 재시작 할 준비가 될때, 원의 크기와 불투명도를 초기화하는 것을 잊지 마세요. restartAnimation()에, handleAnimations()를 다시 호출하기 바로 전에 이것을 추가하세요.

self.coverCircleAlpha = 0
self.coverCircleScale = 1

이제 앱을 빌드하고 실행합니다. 여러분이 구현하기로 결정한, 복잡하고, 완전이 멋진 애니메이션을 모두 구현하였습니다. 여러분 스스로 잘했다고 해주세요. 이것은 사소하지 않았지만 여러분은 그것을 전부 만들었습니다.

이제 의자에 앉아 휴식을 취하고 특히 실제 앱에서 기억해야 할 몇가지 마무리 작업을 완성하러 갑시다.

마무리 작업(Finishing Touches)

여러분이 만든 애니메이션은 굉장히 멋지지만, 현재 구현한 방식으로, 그 애니메이션은 스플래쉬 화면이 사라진 뒤에도 자체적으로 계속 반복할 것입니다.

이것은 대단하지 않습니다. 여러분은 스플래쉬 화면이 사라진 뒤에 계속하는것이 아무런 의미가 없으므로 애니메이션을 막아야 합니다. 어쨌든 사용자에게는 보이지 않지만 불필요한 자원을 사용합니다.

애니메이션이 필요이상으로 더 길게 보여지지 않도록 멈추기 위해서, SplashScreen에 새로운 정적 변수를 추가합니다.

static var shouldAnimate = true

handleAnimations()에서, if문으로 restartAnimation()을 감싸며, 새로운 Boolean이 false이면 다시 시작되지 않습니다. 그것은 다음과 같이 보입니다.

if SplashScreen.shouldAnimate {
  restartAnimation()
}

이제, ContentView.swift로 돌아가서, 시작할때 주석처리했던 .onAppear클로져의 주석을 제거하고 shouldAnimate를 false로 설정합니다. 그리고나서, 재미로, 두번째 상수를 10으로 변경해서 여러분이 만들었던 아름다운 스플래쉬 화면 애니메이션을 즐길수 잇을 것입니다. 그것은 다음과 같이 보입니다.

.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
      SplashScreen.shouldAnimate = false
      withAnimation() {
        self.showSplash = false
      }
    }
}

앱을 빌드하고 실행합니다.

10초 동안 멋진 스플래쉬 화면이 표시되고나서, 앱의 메인 지도 뷰가 나타납니다. 스플래쉬 화면이 사라지면, 배경은 더 이상 애니메이션하지 않는 다는 것이 가장 중요하므로, 사용자는 배경 애니메이션으로 속도가 줄어들지 않고도 앱을 자유롭게 경험할 수 있습니다. 기분 좋습니다!

여기에서 어디로 가야 하나요?(Where to Go From Here?)

Download Materials

이 튜토리얼의 상단과 하단에 있는 Download Materials를 사용해서 시작 프로젝트를 다운로드 할 수 있습니다.

애니메이션에 대한 자세한 내요을 배우고자 하는 경우에, iOS Animations by Tutorials를 확인하세요.

'SwiftUI' 카테고리의 다른 글

How to Create a Splash Screen With SwiftUI  (0) 2019.09.06
SwiftUI  (0) 2019.07.23
SwiftUI: Getting Started  (0) 2019.06.27
Posted by 까칠코더