iyOmSd/Title: SwiftUI

[SwiftUI] 뷰 상태변경 @State @Binding @ObservedObject @StateObject

냄수 2021. 4. 11. 19:23
반응형

SwiftUI를 사용한다면 꼭 알아야할 Property Wrapper중

상태변화에 대한것을 알아보려고해요

 

@State

UIKit에선 Property Observer를 통해서 변화가 일어나면 뷰를 업데이트시키는 방식을 사용했지만

SwiftUI에선 @State라는 프로퍼티 래퍼를 통해서 같은 일을 할 수 있어요

 

@State로 선언된 변수의 값이 변할 때 View를 다시 계산해서 그려줘요

 

주의사항으로는

View의 body에서만 @State변수에 접근해야해요

즉, private 선언이 따라오는것을 권장하고

외부에서 이 변수에 접근하면 안돼요

경고문, 텍스트필드, 편집모드같이 현재화면의상태를 잠깐 나타내거나

간단한 View의 상태를 나타낼 변수를 선언하는데 적합하죠

 

 

SwiftUI에서 View는 struct형태로 구조체 타입이에요

그리고 화면에 보여지는 실질적인 뷰인 body변수가 get-only라는점..!

View의 내용을 수정할 수 없어요!!

그럼 View의 상태를 어떻게변경해서 보여줄까요,,,?

 

구조체 타입이라서 참조를 가지고 있지 않기 때문에

변경사항을 적용해서 그려줄 때 원래 View에 추가하는 방식이아니라

View를 새로 그려주는 방식을 취하죠

 

새로 그려주는데 복사를 해서 수정을 하려해도 변경을 해야하잖아요..!

body가 읽기전용인데 어떻게 수정하나요...?

이때 사용하는게 @State 이겠죠? ㅎㅎ

 

@State변수는 Heap에 할당되요

View에는 포인터만 있고 새로운 View가 만들어지면 포인터를 새로운 뷰로 옮겨서

힙의 같은메모리를 가르킨다면 데이터도 같겠죠?

이런식으로 View의 상태를 저장하고 변경합니다!

 

예를들어

let someClass = AClass()이렇게 class를 생성한다면

someClass는 let으로 읽기전용이지만

someClass에 접근해서 속성을 변경할 순 있잖아요?

이런 느낌이에요

 

 

@Binding

@State의 변수에 $를 붙여서 사용할 수 있는데요

프로퍼티 래퍼에 $를 붙이면 projectValue라고도 해요

$를 붙이면 Binding 타입의 변수가 나타나는것을 볼 수 있어요

 

쉬운 이해를 위해서 코드와 같이 볼게요

struct TestView: View {
    @State private var name: String = "parent"
    
    var body: some View {
        ChildView(childName: $name)
        Text(name)
    }
}


struct ChildView: View {
    @Binding var childName: String
    
    var body: some View {
        Button("Change Name") {
            childName = "child"
        }
    }
}

위의 코드에서

TestView가 ChildView를 포함하고있죠

여기서 현재 TestView의 name은 parent네요

ChildView에서 childName도 parent겟죠?

 

이때!

ChildView에있는 버튼을 눌러서 childName변경한다면 어떤 일이 일어날까요?

 

childName만 child로 변경되지않습니다!

TestView에 있는 name도 child로 변경돼요

 

이렇듯 State의 $는 다른 변수에 연결해주는 역할을 해요

즉, Binding은 다른 어딘가에 연결되어있는 값이고 

해당값이 변경되면 연결된 모든 값들이 변경되죠

데이터에 대한 단일 소스를 갖는 형태죠

 

같은 데이터를 가지고있다면

한쪽에서 수정되면 당연히 같이 수정이 일어나야 되겠죠?

 

 

@ObservedObject

대부분 ViewModel을 선언할 때 사용하는 프로퍼티래퍼에요

ObservableObject 프로토콜을 준수하는 타입에 사용할 수 있어요

 

ObservableObject 프로토콜을 준수하면

objectWillChange.send() 라는 함수가 생기고

이 함수를 호출하면

뷰를 다시 그려요

 

이렇게 하나하나 호출하기 힘들고 언제 변경되는지 찾아야하기때문에

@Published를 사용해서 선언된 변수가 변경되면 자동으로 뷰를 다시 그려주도록 간편하게 사용하죠

 

ViewModel에서 변경사항이 있다면 뷰를 다시 그릴수 있도록 해주는 역할을 하죠

2021.01.29 - [iyOmSd/Title: SwiftUI] - [SwiftUI] Published, ObservableObject

자세한 글은 위의 게시글을 보고오세요!

 

 

@StateObject

기능은 위에서 본 ObservedObject와 같아요

단점을 보완해서 iOS14에서 추가된 기능이에요

 

State + ObservedObject 느낌이에요

 

차이점으로는

ObservedObject와 StateObject로 선언된 ViewModel이 각각 있다면

ObservedObject는 View가 새로 그려질때 새로 생성될 수 있어요 (View의 라이프사이클에 의존)

하지만 StateObject는 View가 새로 그려질때 State처럼 새로그려지지않고 참조를 가지고있어서 새로 생성되지않아요 (View의 라이프사이클에 의존하지 않음)

 

'새로 생성된다' 라는 차이가 있죠 

이 부분은 크게 느껴지는 차이에요

 

1. 데이터가 유실될 수 있는 문제점

2. ViewModel이 생성될 때 작업이 많다면 비효율적인 성능

 

 

말로는 와닿지 않을 수도 있기때문에 코드로 테스트해봤어요

 

단순하게 1씩 증가시키는 기능인 클래스를 만들었구요

이 클래스를 각각 @ObservedObject, @StateObject로 선언하면서 비교해볼거에요

class TimerCount: ObservableObject {
    @Published private(set) var count: Int = 0
    
    init() {
        print(#function)
    }
    
    func plus() {
        count += 1
    }
}

 

 

먼저 ObservedObject를 실험해볼건데요

TestView가 CountView를 가지고있고

업데이트 버튼을 누르면

isUpdate프로퍼티에 의해서 색이 변경되야하기때문에

부모뷰인 TestView가 새로 그려질거에요

 

CountView는 ObservedObject클래스를 가지고있고

버튼을누르면 숫자를 증가시키고

해당숫자를 보여주도록 설계해봤어요

struct TestView: View {
    @State private var isUpdate: Bool = false
    var body: some View {
        VStack {
            CountView(timer: TimerCount())
                .background(isUpdate ? Color.red : Color.orange)
            Divider()
            Button("update") {
                isUpdate.toggle()
            }
        }
    }
}

struct CountView: View {
    @ObservedObject var timer: TimerCount
    
    var body: some View {
        VStack {
            Text("\(timer.count)")
            Button("plus") {
                timer.plus()
            }
        }
    }
}

update가 일어나면 ObservedObject클래스가 가지고있는 count변수가 초기화가 일어나는것을 볼 수 있죠

생성자에 로그를 찍어보면 update시 새로 생성되는 것을 볼 수있어요

 

이런 경우 데이터유실이 일어나서

원하는 데이터를 보여주지 못하는 상황이겟죠?

색만 변경하고싶은데 데이터가 다시 0으로 초기화됬으니말이죠


 

다음으로는 StateObject를 테스트해볼거에요

코드는 그대로구요

변경한 코드는 딱 하나

ObservedObject에서 StateObject로 변경만 했어요

struct CountView: View {
    @StateObject var timer: TimerCount
//    @ObservedObject var timer: TimerCount
    
    var body: some View {
        VStack {
            Text("\(timer.count)")
            Button("plus") {
                timer.plus()
            }
        }
    }
}

원하는 구현이 나왔네요!

색만 변경되고 데이터는 유실되지않았어요

 

이 처럼 뷰가 새로그려질때 ObservedObject 클래스도 새로 생성된다면 큰 문제가 생길 수 있죠

이러한 문제점을 보완해서 생긴것이 StateObject 같네요!


iOS13에서는 ObservedObject만 사용할 수 있는데.. 개발못하나요,,?

물론 아니겠죠 ? ㅎㅎ

 

 

아래는 클래스를 생성하는 방식을 변경한 코드에요

viewModel을 외부에서 선언하고 참조를 넣어주는 경우에는

위의 문제처럼 생성이 일어나지않아요

미리 생성해놓고 참조를 전달하는 방식이기 때문이죠

class의 참조를 전달하는식이기 때문에

StateObject처럼 작동하죠

struct TestView: View {
    @State private var isUpdate: Bool = false
    @ObservedObject var viewModel = TimerCount()  // 변경부분
    var body: some View {
        VStack {
            CountView(timer: viewModel) // 변경부분
                .background(isUpdate ? Color.red : Color.orange)
            Divider()
            Button("update") {
                isUpdate.toggle()
            }
        }
    }
}

struct CountView: View {
    @ObservedObject var timer: TimerCount
    
    var body: some View {
        VStack {
            Text("\(timer.count)")
            Button("plus") {
                timer.plus()
            }
        }
    }
}

간단한 경우에는 클래스를 내부에서 생성해줄 수 있겠지만

필요한 속성이 있는경우 생성자에 상수값을 넣어줄 수 없는경우

동적으로 생성해야하는 경우에는 이 코드처럼 미리 만들어두고 참조를 전달하는 식을 하기위해 복잡해 질 수도 있겠죠

그렇기 때문에 StateObject를 이용해서 간단하게 해결 할 수 있어요

 

 

 

반응형