apple/SwiftUI & Combine

[SwiftUI 3.0] State/ Binding / EnvironmentObject

lgvv 2022. 5. 19. 15:10

State/ Binding / EnvironmentObject

 

✅ 오늘은 이 세가지에 대해서 알아보자.

@State: 값이 변경되었을 때, 화면을 다시 보여주어야 할 때.

@Binding: state같은 친구들을 view - view 간에 공유해야 하는 경우.

@EnvironmentObject: parent로 child(하위) view에 공유해야 하는 경우.

 

우선 State와 Binding에 대해서 이해해보자.

 

✅ 예시 코드

struct FirstView: View {
    
    @State var appTitle = tabIndex.first.rawValue
    @State var count = 0
    
    enum tabIndex: String {
        case first = "1번뷰 입니다."
        case second = "2번뷰 입니다."
        case third = "3번뷰 입니다"
    }
    
    var body: some View {
        
        TabView {
            VStack {
                Text("스위프트 유아이를 정복해봅시다. count: \(count)")
                    .padding()
                Button { // action
                    count += 1
                } label: {
                    Text("카운트 업")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(10)
                }
            }
            .tabItem { Label("1번 뷰", systemImage: "pencil.circle") }
                
            SecondView(count: $count)
                .tabItem { Label("2번 뷰", systemImage: "pencil.circle") }
            
            ThirdView(count: $count)
                .tabItem { Label("3번 뷰", systemImage: "pencil.circle") }
        }
    }

}

 

✅ SecondView (ThirdView도 코드가 같습니다.) 

import SwiftUI

struct SecondView: View {
    @Binding var count: Int
    
    init(count: Binding<Int> = .constant(0)) {
        _count = count
    }
    
    var body: some View {
        VStack {
            Text("2번뷰입니다. \(count)")
            Button { // action
                count += 1
            } label: {
                Text("카운트 업")
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.blue)
                    .cornerRadius(10)
            }
        }
        
    }
}

 

세개의 뷰

 

🟠 1번뷰(@State를 갖고 있음)에서 카운트업을하고 2번뷰로 이동하면 1,2번 뷰 모두 카운트가 달라집니다.

🟠 그런데 @Binding으로 처리되어 있는 2번뷰에서 카운트 업을하고 1번뷰로 간다면? 

  -> 이 경우에도 카운트가 달라집니다.

🟠 그렇다면 2번뷰에서 카운트를하고 3번뷰로 이동을 바로 한다면?

 -> 이 경우에도 카운트가 달라지게 됩니다.

 

State와 Binding은 뷰 간에 데이터를 공유하게 해줍니다.

State에 걸린 동일한 Binding에 전부 반영된다고 이해할 수 있습니다.

 

 

 

다음으로 EnvironmentObject에 대해서 알아봅시다.

 

✅ @State의 경우에는 뷰 전체를 다시 그리는 동작을 합니다.

예를 들어서 row가 10인 List를 그렸다고 가정합시다. row를 클릭하면 해당 row의 데이터를 변경하여 UI를 갱신한다고 생각합시다.

그렇다면 다시 다 그려야하니까 비용이 넘 큽니다... 해당 row만 바꿔주면 훨씬 더 효율적이겠죠?

또한, Combine이라는 개념을 도입해야 하는데, 이 경우에는 RxSwift를 공부하던 기억을 살리면 쉽게 접근할 수 있습니다.

이렇게 되면은 하위에서 구독의 개념이 생겨서 하위뷰 전체를 동시에 컨트롤 할 수 있게 된다.

잘 이해가 안간다면 코드를 보자!

 

🟠 사용법 정리

1. ObservableObject를 ViewModel에서 상속받기

2. 해당 View에 environment를 사용하여 ViewModel을 넣어주기

 

 

SwiftUI_PracticeApp

@main
struct SwiftUI_PracticeApp: App {
    var body: some Scene {
        WindowGroup {
        	// viewModel을 넣어준다.
            FirstView().environmentObject(ViewModel()) 
            
        }
    }
}

 

✅ ViewModel

import Foundation
import Combine

// ObservableObject는 이벤트 처리에 대한 것을 상속
class ViewModel: ObservableObject {
    @Published var appTiltle = ""
}

 

FirstView

import SwiftUI

struct FirstView: View {
    
    @EnvironmentObject var viewModel: ViewModel
    @State var appTitle = tabIndex.first.rawValue
    @State var count = 0
    
    enum tabIndex: String {
        case first = "1번뷰 입니다."
        case second = "2번뷰 입니다."
        case third = "3번뷰 입니다"
    }
    
    var body: some View {
        
        TabView {
            VStack {
                Text(appTitle)
                Text("스위프트 유아이를 정복해봅시다. count: \(count)")
                    .padding()
                Button { // action
                    count += 1
                    viewModel.appTiltle = "\(appTitle)\(count)"
                } label: {
                    Text("카운트 업")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(10)
                }
            }
            .tabItem { Label("1번 뷰", systemImage: "pencil.circle") }
                
            SecondView(count: $count)
                .tabItem { Label("2번 뷰", systemImage: "pencil.circle") }
            
            ThirdView(count: $count)
                .tabItem { Label("3번 뷰", systemImage: "pencil.circle") }
        }
        .onReceive(viewModel.$appTiltle) { appTitle in
            print("Receieved", appTitle)
            self.appTitle = appTitle
        }
    }
}

 

✅ SecondView

import SwiftUI

struct SecondView: View {
    @EnvironmentObject var viewModel: ViewModel
    @Binding var count: Int
    @State var appTitle: String = ""
    
    init(count: Binding<Int> = .constant(0)) {
        _count = count
    }
    
    var body: some View {
        VStack {
            Text(viewModel.appTiltle)
            Text("2번뷰입니다. \(count)")
            Button { // action
                count += 1
                viewModel.appTiltle = "\(appTitle)\(count)"
            } label: {
                Text("카운트 업")
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.blue)
                    .cornerRadius(10)
            }.onReceive(viewModel.$appTiltle) {
                self.appTitle = $0
            }
        }
        
    }
}

onReceive하고 있기 때문에 12, 1213, 121314 ... 이런식으로 UI에 반영된다.

 

✅ ThirdView

import SwiftUI

struct ThirdView: View {
    @EnvironmentObject var viewModel: ViewModel
    @Binding var count: Int
    @State var appTitle: String = ""
    
    init(count: Binding<Int> = .constant(0)) {
        _count = count
    }
    
    var body: some View {
        VStack {
            Text(viewModel.appTiltle)
            Text("3번뷰입니다. \(count)")
                .padding()
            Button { // action
                count += 1
                viewModel.appTiltle = "\(appTitle)\(count)"
            } label: {
                Text("카운트 업")
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.blue)
                    .cornerRadius(10)
            }
            
        }
    }
}

여기서는 onReceive가 없어서 이전 값을 처음에 한번만 가져오고 이후에 지속해서 값을 받아들이지 못한다.

따라서 카운트업을 눌러도 appTitle이 빈 상태라서 12, 13, 14 .. 이런식으로 UI에 반영된다.