본문 바로가기

SwiftUI

[SwiftUI] Transition사용법 심층정리 - 주니오스의 iOS어드벤쳐

개요

안녕하세요! 

 

SwiftUI를 처음 배울당시 Transition을 뷰에 적용하는 것에 많은 시행착오를 격었었습니다. 🥹 

 

명확하게 글로 정리하여 참고하면 좋을 것 같아 글을 작성하였습니다!

 

 

 

Transition이 사용될 수 있는 경우

기본적으로 Transition은 랜더링되어 있는 뷰가 사라지거나 나타나는 경우에 사용될 수 있습니다.

자주 사용되는 경우로는 아래 3가지경우가 있습니다.

 

 

  • if문 사용
  • switch문 사용
  • List, Foreach에 전달된 아이템 소스의 변동

 

Transition사용하기

Transition은 기본적으로 애니메이션과 함께 사용되어야 합니다.

하지만 아시다시피 애니메이션을 적용할 수 있는 방법은 크게 2가지 있습니다.

 

 

명시적 애니메이션(explicit animation)

withAnimation 매서드를 사용하는 애니메이션을 적용하는 것을 명시적 애니메이션 이라고 합니다.

아래 코드는 버튼을 누르면 Text뷰가 등장하는 간단한 코드입니다. 기본 Transition중 하니인 slide를 적용하였습니다.

@State var isShowing = false

...

if isShowing {
    Text("if문을 사용한것")
        .padding(10)
        .background(
            Rectangle()
                .foregroundColor(.cyan)
        )
        .transition(.slide)
}

...

Button(isShowing ? "꺼져라!" : "나타나라!") {
    withAnimation {
        isShowing.toggle()
    }
}

Transition이 잘적용된 것을 확인할 수 있습니다. 

 

 

암시적 애니메이션(Implicit animation)

animation수정자를 사용하는 암시적 애니메이션 방식도 Transition을 가능하게 할 수 있습니다.

다만 AnyTransition기본 프로퍼티의 일부는 이방식이 적용되지 않습니다.

 

 

  • 사용 가능 전환: slide, asymmetric
  • 사용 불가 전환: scale, opacity

 

 

AnyTransition.modifier로 생성한 커스텀 인스턴스의 경우 해당 방식이 잘 적용됩니다.

커스텀 인스턴스에 대해서는 후에 다루도록 하겠습니다. 

 

그리고 이방식을 사용하려면 한가지 조건이 더 존재합니다. 바로 수정자들이 뷰에 직접 적용되는 것이 아니라 외부에 위치해야 합니다.

말이조금 어렵습니다. 아래코드를 봐주세요.

Group {
    if isShowing {
        Text("if문을 사용한것")
            .padding(10)
            .background(
                Rectangle()
                    .foregroundColor(.cyan)
            )
            .transition(.slide)
    }
}
.animation(.easeOut, value: isShowing)

Group에 대해서는 컨테이너와 Transition에서 다루겠습니다.

 

사라지는 뷰의 밖에 animation수정자를 위치시키면 잘 동작합니다.

if문 내부에 해당수정자를 위치시킬경우 Transition이 적용되지 않습니다.

 

 

animation인스턴스 매서드

AnyTransition의 인스턴스 매서드인 animation을 사용하여 전환효과를 부여할 수 있습니다.

if isShowing {
    Text("if문을 사용한것")
        .padding(10)
        .background(
            Rectangle()
                .foregroundColor(.cyan)
        )
        .transition(.opacity.animation(.easeInOut))
}

깔금하게 코드를 작성할 수 있음으로 장점이 있는 방식입니다.

 

 

컨테이너와 Transition

앞서 적용한 Transition은 모두 기본 뷰에 직접 적용한 것입니다. 컨테이너에는 Transition이 어떻게 적용될까요? 바로 알아봅시다.

 

먼저 컨테이너의 종류에 따라 적용되는 방식이 나뉩니다.

 

 

V, H, ZStack

VStack, HStack, ZStack에 Transition을 사용할 경우 해당 컨테이너 전체에 효과가 적용됩니다. 즉, 이 3가지 컨테이너 내부에 포함된 하위 뷰들에 개별적으로 효과가 적용되는 것이 아니라 컨테이너 전체가 사라지고 나타나야 효과가 적용됩니다.

 

아래 코드는 VStack을 예시로 작성한 코드입니다.

if isShowing {
    VStack {
        Text("if문을 사용한것")
            .padding(10)
            .background(
                Rectangle()
                    .foregroundColor(.cyan)
            )
    }
    .transition(.slide)
}
VStack 전체 적용

그렇다면 코드를 조금 바꿔서 하위 뷰에만 적용되는 지 확인해 보겠습니다.

아래 코드에서 사라지고 나타나는 것은 하위뷰인 Text뷰 뿐입니다.

VStack {
    if isShowing {
        Text("if문을 사용한것")
            .padding(10)
            .background(
                Rectangle()
                    .foregroundColor(.cyan)
            )
    }
}
.transition(.slide)

 

VStack 하위뷰 적용 불발

영상에서 확인 가능하듯이 하위뷰에 개별적으로 적용되지 않습니다.

 

 

 

Group

Group을 사용하여 뷰들을 그룹화할 경우 앞서다룬 컨터이너들과는 달리 개별 뷰에 모두 Transition효과가 적용됩니다.

아래 코드에서 사라지는 것은 Text뷰 뿐입니다.

Group {
    if isShowing {
        Text("if문을 사용한것")
            .padding(10)
            .background(
                Rectangle()
                    .foregroundColor(.cyan)
            )
    
    }
}
.transition(.slide)

 

 

Group에 사용

 

하위 뷰에 개별적으로 잘 적용된 것을 확인할 수 있습니다. 가장 추천하는 패턴입니다.

 

 

 

 

List, Foreach

List의 경우 개별요소에 여러시도를 해보았지만 Transition을 적용할 수 없었습니다.

List {
    ForEach(listItem, id: \.self) { element in
        Text(element)
            .foregroundColor(.white)
            .padding(10)
            .background(
                Rectangle()
                    .foregroundColor(.indigo)
            )
    }
}
.transition(.scale)
List에 적용한 전환효과

영상에서 확인 가능하듯이 코드에 작성한 scale효과가 적용되지 않는 것을 확인할 수 있습니다.

 

 

Foreach는 Group처럼 모든 요소에 개별적으로 전화효과를 적용할 수 있습니다. 

다만, VStack이 아닌 List로 감싸는 경우 Transition효과가 적용되지 않습니다.

VStack {
    ForEach(listItem, id: \.self) { element in
        Text(element)
            .foregroundColor(.white)
            .padding(10)
            .background(
                Rectangle()
                    .foregroundColor(.indigo)
            )
    }.transition(.scale)
}
.animation(.easeOut(duration: 2.0), value: isShowing)
Froach + VStack

등장/퇴장 별개의 전환효과 적용하기

기본적으로 transition수정자는 하나의 인스턴스만을 전달받습니다. 그리고 전달한 인스턴스는 등장과 퇴장시 모두 적용됩니다. 

AnyTransition.asymmetric매서드를 사용하면 등장/퇴장시 별개의 전환효과를 적용할 수 있습니다.

 

 

 

 

커스텀 Transition

AnyTransition.modifier매서드를 사용하여 커스텀 전환효과를 만들 수 있습니다.

먼저 아래코드처럼 extension을 활용합니다.

 

extension AnyTransition {
    static func customSlide(from: CGPoint, to: CGPoint) -> AnyTransition {
        return AnyTransition.modifier(active: CustomSilde(coordi: from), identity: CustomSilde(coordi: to))
    }
}

 

위 코드를 보면 modifier매서드를 사용한 것을 확인할 수 있습니다.

modifier매서드는 AnyTransition인스턴스를 반환하는 매서드입니다.

active, identity는 인수로 ViewModifier객체를 전달받는다.

 

여기서 active, identity는 각기다른 2가지 상태를 의미합니다.

 

두상태를 A, B라고 칭하겠습니다.

 

등장 시 A상태에서 B상태로 뷰의 상태가 변화합니다.

 

 

이런 상태변화를 통해 전환효과를 만들어 낼 수 있는 것입니다!

 

 

 

두 매개변수는 insertion으로 사용될 경우와 removal로 사용될 경우 혼동이 발생할 수 있습니다. 

여기서 초기상태란 상태변이가 발생하는 초기상태, 종착상태란 상태변이가 완료된 시점의 상태를 의미합니다.

 

 

  • insertion/removal에 상관없이(2024.2.13 수정)
    • active는 초기상태를 의미하고 identity는 종착상태를 의미합니다.
    • 즉, 뷰가 등장/퇴장시 active상태에서 identity상태로 전환이 발생합니다.
    • 만약 insertion과 removal의 identity로 인한 상태가 다를 경우 removal의 identitiy가 우선됩니다. (주니오스 실험상)

 

 

아래코드는 해당 커스텀 전환을 적용한 예시입니다.

 

insertion의 경우

from -> active

to -> identity

에 할당됨으로 from에 전달된 매개변수 상태에서 to에 전달된 매개변수 상태로 전환이 발생합니다.

 

removal의 경우

from -> active

to -> identity

에 할당됨으로 to에 전달된 매개변수 상태에서 from에 전달된 매개변수 상태로 전환이 발생합니다.

.transition(.asymmetric(
        insertion: .customSlide(from: CGPoint(x: -1000, y: 0), to: CGPoint(x: 0, y: 0)), 
        removal: .customSlide(from: CGPoint(x: 1000, y: 0), to: CGPoint(x: 0, y: 0))
))

 

등장 시: (-1000, 0) → (0, 0)

 

퇴장 시: (0, 0) → (1000, 0)

커스텀 슬라이드

 

살짝햇살지만 insertion, removal이 서로 반대로 작용한다! 로 생각하면 이해에 조금 도움이 될 듯 합니다.

 

 

ViewModifier

앞서 active와 identity에 사용한 ViewModifier 프로토콜에 대해 설명해 보겠습니다.

 

간단히 설명하자면 수정자로 사용될 수 있는 객체를 생성하는데 필요한 프로토콜 입니다.

 

앞서 사용한 CustomSlide객체는 아래 코드처럼 modifier수정자와 함깨 사용될 수 있습니다.

VStack{ ... }
    .modifier(CustomSilde(coordi: CGPoint(x: 100, y: 0)))

아래코드는 CustomSlide의 구현부입니다.

body매서드는 content를 전달받게 되며 content에는 수정자가 적용될 뷰가 전달됩니다.

struct CustomSilde: ViewModifier {
    
    var coordi: CGPoint
    
    func body(content: Content) -> some View {
        content
            .offset(x: coordi.x, y: coordi.y)
    }
    
}

 

 

적용할 뷰에 offset을 변경해서 반환하면 아래 사진처럼 적용되는 것을 확인할 수 있습니다.

 

 

offset이 변경된 뷰

 

 

Transition 방식 설정

앞서 언급했듯 Transition은 상태 A에서 B로의 전환을 의미합니다. 

 

상태전이의 방식은 개발자가 직접 설정할 수 있습니다.

 

아래코드는 슬라이드 상태전환에 spring을 적용한 것입니다.

spring을 적용한 슬라이드

 

 

withAnination방식

withAnimation의 매개변수로 duration이 포함된 애니메이션을 전달하여 속도를 설정한 예시입니다.

withAnimation(.easeOut(duration: 10.0)) {
    isShowing.toggle()
}

이방법은 isShowing을 사용하는 모든 뷰에 적용되고 다른 상태변화에도 지속시간이 적용됨으로

맞춤형 전환효과를 만들기에는 부적합 합니다.

 

 

animation수정자를 사용하는 방법

animation수정자와 animation인스턴스 매서드를 사용할 경우 특정 전환에 특화된 효과를 만들 수 있습니다.

Group {
    if isShowing {
        Text("if문을 사용한것")
            .padding(10)
            .background(
                Rectangle()
                    .foregroundColor(.cyan)
            )
    }
}
.transition(.asymmetric(insertion: .customSlide(from: CGPoint(x: -1000, y: 0), to: CGPoint(x: 0, y: 0)), removal: .customSlide(from: CGPoint(x:1000, y: 0), to: CGPoint(x: 0, y: 0))))
.animation(.easeOut(duration: 2.0), value: isShowing)

 

animation인스턴스 매서드 사용

Group {
    if isShowing {
        Text("if문을 사용한것")
            .padding(10)
            .background(
                Rectangle()
                    .foregroundColor(.cyan)
            )
    }
}
.transition(.scale.animation(.easeInOut(duration: 3.0)))

이 방법은 전환 인스턴스에 따라 적용되지 않을 수 있습니다!!

적용 가능: opacity, scale, AnyTransition.modifier로 생성한 커스텀 인스턴스, asymmetric

적용 불가: slide

 

 

 

 

자, 이렇게 SwiftUI Transition에 대해 정리해봤습니다.

 

부족한점은 댓글로 알려주시면 감사하겠습니다.

 

좋은 하루 되세요!!