[iOS / SwiftUI] 사용자 인터페이스 상태 관리하기

박준혁 - Niro·2023년 2월 14일
0

SwiftUI

목록 보기
2/7
post-thumbnail
post-custom-banner

안녕하세요 Niro 입니다!

이번엔 SwiftUI 에서 View 끼리 어떻게 데이터를 주고받고 View 를 최신 상태로 업데이트 하는지 전반적인 흐름을 알아보고자 Apple Developer 에 있는 Document 를 살펴 보려고 합니다.

공식 문서에 있는 내용이라 어려운 전문용어와 영어로 되어있어 번역기를 돌려도 해석이 맞지 않는 부분이 많아 애로사항이 많았습니다.

이러한 부분을 잘 전달해 드리고자 최대한 자세히 설명을 적어놓았으니 내용이 많아보여도 천천히 잘 읽어주시면 감사하겠습니다!



1. SwiftUI 의 State 관리 방법

SwiftUI 에서는 데이터를 관리할 때 Single source of truth( SSOT ) 를 만족해야 합니다.

여기서 Single source of truth ( SSOT ) 란 데이터의 값을 결정하는 곳이 단 하나의 위치 에서만 존재 해야한다는 정보 시스템 설계의 이론입니다.

예를 들어 여러 개의 View 에서 사용자의 이름을 결정할 수 있다면 각 View 에서 사용자의 이름이 다 다르겠죠?

또한 이름을 결정하는 기능의 코드들이 중복 될 가능성도 높습니다.

즉, 사용자 이름이 필요한 View 에서는 이름을 결정하는 단 하나의 위치에서만 값을 참조해야 한다는 것 입니다.

결과적으로 참조 했기 때문에 어떠한 View 에서 이름을 수정하던 간에 모든 View 에서 이름이 동기화 됩니다.

다음과 같은 기능을 수행하고자 SwiftUI 에서는 공통으로 사용하는 값을 최소한의 부모 View 에서 접두사로 State 를 붙여 property 를 선언하게 됩니다.

또한 Swift property 를 통해 데이터를 읽기 전용으로 제공하거나 Binding 을 사용하여 State 에 대한 양방향 연결을 제공합니다.

즉, SwiftUI 는 변경되는 데이터를 감시하고 필요에 따라 영향을 받는 View 를 업데이트 합니다.

주의할 점으로는 단순히 데이터를 저장만을 위해서 State property 를 사용해서는 안된다고 합니다. State 변수의 수명 주기는 View life Cycle 을 반영하기 때문입니다.

대신에 Button 의 강조 표시 상태, Fliter setting, 현재 선택된 목록 등과 같은 사용자 인터페이스에서만 영향을 미치는 일시적인 상태를 관리하는데 사용을 권장합니다.

더 자세히 알아볼까요?



2. 변경 가능한 값을 State 로 관리하세요!

View 에서 수정할 수 있는 데이터를 저장해야 하는 경우 State property 를 선언해야 합니다.


sturct PlayerView: View {
	@State private var isPlaying: Bool = false
    
    var body: some View {
		// ...
    }
}

예를 들어 podcast 를 보고 있는 중이라면 View 에서 Bool 형 State 변수를 생성하여 podcast 가 실행중일 때 추적할 수있게 됩니다.

Property 를 State 로 선언하면 기본 저장소를 관리하도록 Framework 에 알리게 됩니다. View 는 State Property 에서 찾은 데이터를 읽고 씁니다.

값을 변경하게 되면 SwiftUI 는 View 의 영향을 받는 부분을 업데이트 합니다.

또한 State 변수를 private 로 선언하며 범위를 제한할 수 있습니다. 다음과 같이 선언하게 되면 변수를 선언하는 View 계층 구조에서 변수가 캡슐화 된 상태로 유지하게 됩니다.


Button(action: {
	self.isPlaying.toggle()
}) {
	Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}

예를 들어 Button 을 탭하게 되면 .toggle() 메서드를 통해 저장된 값을 바꾸고 저장된 값에 따라 다른 이미지를 표시하는 Button 을 추가할 수 있습니다.



3. 변경할 수 없는 값을 저장하기 위한 Swift property 선언하기

View 에서 바뀌지 않은 데이터를 View 에 제공하기 위해선 표준 Swift property 를 선언해야 합니다.

라고 적여있지만 너무 어렵게 설명 되어있죠...?

일단 표준 Swift property 은 property 를 선언할 때 사용하는 표준으로 var 와 let 로 둘 중 하나로 선언해야 합니다.

여기서 바뀌지 않는 데이터는 변수의 값이 바뀌지 않는 상수를 의미하기 때문에 let 으로 선언 해주어야 합니다.


struct PlayerView: View {
    let episode: Episode // The queued episode.
    @State private var isPlaying: Bool = false
    
    var body: some View {
        VStack {
            // Display information about the episode.
            Text(episode.title)
            Text(episode.showTitle)

            Button(action: {
                self.isPlaying.toggle()
            }) {
                Image(systemName: isPlaying ? "pause.circle" : "play.circle")
            }
        }
	}
}

예를 들어 에피소드 제목과 프로그램 이름에 대한 문자열을 포함하는 입력 구조를 갖도록 Podcase Player 를 확장 할 수 있습니다.

episode property 값은 PlayerView 에서 let 으로 선언됐기 때문에 상수이지만 해당 View 의 부모 View 에서는 상수 일 필요가 없습니다.

즉, 사용자가 부모 View 에서 다른 에피소드를 선택하면 SwiftUI 는 변경된 상태를 감지하고 새 입력으로 PlayerView 를 다시 만들게 됩니다.



4. Binding 을 사용하여 State 에 대한 접근 공유

부모 View가 자식 View 와 상태를 공유해야 하는 경우 Binding property 를 사용하여 자식 View 에서 property 를 선언합니다.

Binding 은 기존 저장소에 대한 참조를 하고 기본 데이터에 대한 Single source of truth( SSOT ) 를 유지하게 됩니다.

즉, 부모 View 있는 State property 와 자식 View 의 Binding property 는 서로 연결 되어있다는 뜻입니다!


struct PlayButton: View {
    @Binding var isPlaying: Bool
    
    var body: some View {
        Button(action: {
            self.isPlaying.toggle()
        }) {
            Image(systemName: isPlaying ? "pause.circle" : "play.circle")
        }
    }
}

예를 들어 View 의 Button 을 PlayButton 이라는 자식 View 로 분할한 경우 PlayerView 이라는 부모 View 에 isPlaying property 의 Binding 을 지정할 수 있습니다.

위의 코드처럼 State 와 마찬가지로 property 에 직접 참조하여 Binding 의 래핑된 값을 읽고 쓰게 됩니다.

즉, PlayerView 에서 isPlaying State 변수와 PlayButton 에서 isPlaying Binding 변수는 서로 양방향 소통을 하고 있는거죠!

그러나 State property 와 달리 Binding 은 저장소가 없습니다. 대신 다른 위치에서 저장된 State property 를 참조하고 해당 저장소에 대한 양뱡향 연결을 제공합니다.


struct PlayerView: View {
    var episode: Episode
    @State private var isPlaying: Bool = false
    
    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(isPlaying: $isPlaying) // Pass a binding.
        }
    }
}

부모 View 인 PlayerView 를 관점으로 본다면

PlayButton 을 인스턴스화 할 때 달러($) 기호를 접두사로 지정하여 부모 View 에서 선언된 해당 State 변수에 대한 양방향 연결을 제공하게 됩니다.

즉, 인스턴스화 된 PlayButton 에서 파라미터 값으로 들어간 $isPlaying 은 부모 View 의 State 변수인 isPlaying 과 연결이 된다는 것이죠!

또한 Binding property 에서 데이터를 가져올 수 있으므로 View 계층 구조를 통해 데이터를 전달할 수 있습니다.



5. 상태 전환 애니메이션

앞서 말씀 드린것 처럼 View 의 상태가 변경되면 SwiftUI 는 영향을 받는 View 를 즉시 업데이트 하게 됩니다.

업데이트를 할때 시각적인 전환을 매끄럽게 하기 위해서 withAnimation() 함수 호출에서 상태 변경을 하여 애니메이션 효과를 주도록 SwiftUI 에 지시할 수 있습니다.

withAnimation(.easeInOut(duration: 1)) {
    self.isPlaying.toggle()
}

예를 들어 Bool 형인 isPlaying 으로 제어되는 변경사항을 통해 애니메이션을 만들 수가 있습니다.

Animation 함수에서 재생을 변경하면 Button 이미지 크기 조정과 함께 의존하는 모든 항목에 적용할 수 있도록 SwiftUI 에 지시할 수 있습니다.

Image(systemName: isPlaying ? "pause.circle" : "play.circle")
    .scaleEffect(isPlaying ? 1 : 1.5)

동일한 isPlaying property 를 통해 표시할 이미지를 삼항연사지로 지정하더라도 이미지 컨텐츠는 애니메이션에 영향을 받지 않습니다.

SwiftUI 가 pause.circle, pause.circle 두 문자열 간에 의미있는 방식으로 점진적으로 전환할 수 없기 때문입니다.

즉, 그냥 이미지가 짠 하고 바뀐다는 의미 입니다!

해결방법으로는 State Property 에 애니메이션을 추가하거나 위의 예제와 같이 Binding 에 애니메이션을 추가할 수 있습니다.



6. 정리하자면!

SwiftUI 에서는 View 간에 어떻게 데이터를 관리하고 View 를 최신화 시키는지 Document 를 통해서 알아보았습니다.

  1. 변경 가능한 값을 State 로 property 를 선언합니다.
  2. Binding 을 사용해서 State 와 양방향 소통을 통해 View 끼리 연결을 시켜줍니다.
  3. SwiftUI 는 State 로 선언된 property 의 값이 바뀌면 자동으로 View 를 업데이트 합니다.
  4. 단순히 저장을 위한 변수는 State 로 선언하지 않습니다!

이 부분만 충분히 익히고 기억하면서 즐거운 코딩을 하면 될거 같습니다.

잘못된 부분이 있다면 댓글로 남겨주시고 더 좋은 글로 찾아올게요!
감사합니다!

profile
📱iOS Developer, 🍎 Apple Developer Academy @ POSTECH 1st, 💻 DO SOPT 33th iOS Part
post-custom-banner

0개의 댓글