ObservableObject 와 SwiftUI에서의 MVVM 구현

해류·2023년 4월 27일
0

SwiftUI 개념

목록 보기
1/1

ObservableObject

ObservableObject는 SwiftUI에서 데이터를 관찰하고 변경 사항을 자동으로 반영할 수 있는 객체를 정의하는 프로토콜입니다. ObservableObject 프로토콜을 채택하면 해당 객체를 데이터 바인딩에 사용할 수 있으며, 해당 객체의 프로퍼티가 변경될 때마다 연결된 뷰가 자동으로 업데이트됩니다.

ObservableObject를 사용하려면 다음을 수행해야 합니다:

  1. ObservableObject 프로토콜을 채택하는 클래스를 정의합니다.
  2. 관찰 가능한 프로퍼티에 @Published 속성 래퍼를 추가하여 해당 프로퍼티의 변경 사항이 연결된 뷰에 자동으로 전파되도록 합니다.
  3. 해당 객체를 사용하는 뷰에서 @ObservedObject 또는 @StateObject 속성 래퍼를 사용하여 관찰 가능한 객체를 프로퍼티에 바인딩합니다.

@ObservedObject@StateObject의 차이점은 다음과 같습니다:

  • @ObservedObject: 부모 뷰에서 생성된 인스턴스를 사용해야 할 때 사용합니다. 부모 뷰가 다시 렌더링될 때 해당 인스턴스가 새로 생성될 수 있습니다.
  • @StateObject: 뷰 자체에서 생성되는 인스턴스를 사용하거나 뷰의 생명 주기 동안 인스턴스가 유지되어야 할 때 사용합니다. 뷰가 다시 렌더링되어도 인스턴스가 새로 생성되지 않습니다.

간단한 예시로, ObservableObject 프로토콜을 채택하는 클래스를 작성하고, SwiftUI 뷰에서 해당 객체를 사용하는 방법을 살펴보겠습니다.

import SwiftUI
import Combine

class Counter: ObservableObject {
    @Published var count = 0
}

struct CounterView: View {
    @ObservedObject var counter = Counter()

    var body: some View {
        VStack {
            Text("Count: \(counter.count)")
            Button("Increment") {
                counter.count += 1
            }
        }
    }
}

struct CounterView_Previews: PreviewProvider {
    static var previews: some View {
        CounterView()
    }
}

위의 예시에서 Counter 클래스는 ObservableObject 프로토콜을 채택하며, count 프로퍼티에 @Published 속성 래퍼를 추가합니다. 그런 다음 CounterView에서 Counter 객체를 @ObservedObject 속성 래퍼를 사용하여 바인딩합니다. 이렇게 하면 count 프로퍼티가 변경될 때마다 뷰가 자동으로 업데이트됩니다.


@StateObject@ObservedObject는 모두 ObservableObject로 선언된 클래스의 인스턴스를 바인딩하는 방법이지만, 사용하는 상황과 생명주기(lifecycle) 관리 방식에 차이가 있습니다.

@StateObject

@StateObject는 뷰가 소유한 ObservableObject 인스턴스를 생성하고 관리할 때 사용합니다. 뷰의 생명주기와 함께 생성되어 뷰가 파괴될 때까지 유지됩니다. @StateObject는 주로 뷰 모델을 초기화하고 소유할 때 사용됩니다.

예시:

struct MyView: View {
    @StateObject private var viewModel = MyViewModel()

    var body: some View {
        // 뷰 코드
    }
}

@ObservedObject

@ObservedObject는 뷰 외부에서 생성된 ObservableObject 인스턴스를 바인딩할 때 사용합니다. 이것은 주로 부모 뷰에서 생성된 뷰 모델을 자식 뷰로 전달할 때 사용됩니다. @ObservedObject는 뷰가 새로 생성될 때마다 새로운 인스턴스를 만들지 않고 기존 인스턴스를 사용합니다.

예시:

struct ParentView: View {
    @StateObject private var viewModel = MyViewModel()

    var body: some View {
        ChildView(viewModel: viewModel)
    }
}

struct ChildView: View {
    @ObservedObject var viewModel: MyViewModel

    var body: some View {
        // 뷰 코드
    }
}

요약하면, @StateObject는 뷰가 ObservableObject 인스턴스를 소유하고 생성하는 경우 사용하며, @ObservedObject는 뷰 외부에서 생성된 ObservableObject 인스턴스를 바인딩하는 경우 사용합니다. 이 두 속성은 각각 뷰의 생명주기와 연관된 방식으로 인스턴스를 관리합니다.

@StateObject, @ObservedObject, @EnvironmentObject의 차이를 요약하면 다음과 같습니다.

  • @StateObject: 뷰가 인스턴스를 소유하고 관리하는 경우 사용합니다.
  • @ObservedObject: 외부에서 생성된 인스턴스를 바인딩하는 경우 사용합니다. 주로 부모 뷰에서 생성된 뷰 모델을 자식 뷰로 전달할 때 사용됩니다.
  • @EnvironmentObject: 뷰 계층 구조 전체에서 접근 가능한 공유 데이터를 제공하는 데 사용됩니다. 주로 전역 상태를 관리하는 데 사용됩니다.

@EnvironmentObject

@EnvironmentObject는 앱 전체에 공유되는 ObservableObject 인스턴스를 사용할 때 사용합니다. 이를 통해 여러 뷰에서 동일한 데이터 소스를 사용하고 업데이트를 관찰할 수 있습니다. 주로 앱의 전역 상태를 관리하거나 여러 뷰에서 접근해야 하는 데이터를 공유할 때 사용됩니다.

먼저, ObservableObject 인스턴스를 @EnvironmentObject로 지정한 후, 필요한 뷰에서 @EnvironmentObject 프로퍼티 래퍼를 사용하여 접근할 수 있습니다.

예시:

  1. 앱 전체에서 공유할 UserData 클래스를 생성합니다.
class UserData: ObservableObject {
    @Published var username: String = ""
    @Published var age: Int = 0
}
  1. @EnvironmentObjectUserData 인스턴스를 주입합니다.
@main
struct MyApp: App {
    @StateObject private var userData = UserData()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(userData)
        }
    }
}
  1. 필요한 뷰에서 @EnvironmentObject를 사용하여 UserData 인스턴스에 접근합니다.
struct ProfileView: View {
    @EnvironmentObject var userData: UserData

    var body: some View {
        Text("Username: \(userData.username), Age: \(userData.age)")
    }
}

이렇게 하면 ProfileView에서 UserData 인스턴스를 사용하여 사용자 이름과 나이를 표시할 수 있으며, 다른 뷰에서도 동일한 인스턴스를 공유하여 사용할 수 있습니다.


전체 소스 코드 - SwifUI에서의 MVVM 구현

// MVVM 패턴
// M - Model
struct MessageInfo {
    var profileImage: String
    var stack: Int
    var nickname: String
    var contents: String
    var place: String?
    var hour: Int?
    var hasMultiple: Bool
    let cardColor: CardColor
}

// VM - ViewModel
class TimelineViewModel: ObservableObject {
    @Published var messageInfoList: [MessageInfo] = []

    private var cancellables = Set<AnyCancellable>()

    func fetchRecentFeed() {
        FeedService.getRecentFeed { result in
            switch result {
            case .success(let response):
                if let recentFeeds = response.data {
                    print(recentFeeds)
                    self.messageInfoList = recentFeeds.map { recentFeed in
                        // RecentFeed를 MessageInfo로 변환
                        MessageInfo(
                            profileImage: "alien",
                            stack: recentFeed.recentMessageInfo.brushCnt,
                            nickname: recentFeed.recentMessageInfo.nickname,
                            contents: recentFeed.recentMessageInfo.content,
                            place: recentFeed.placeWithTimeInfo.placeName,
                            hour: nil, // 시간을 계산하는 로직이 필요합니다.
                            hasMultiple: (recentFeed.recentMessageInfo.brushCnt > 1),
                            cardColor: CardColor(rawValue: recentFeed.recentMessageInfo.color) ?? .red
                        )
                    }
                }

            case .failure(let error):
                print("Error fetching recent feeds: \(error)")
            }
        }
    }
}

// V - View
struct TimelineView: View {
    @StateObject private var viewModel = TimelineViewModel()

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                if viewModel.messageInfoList.count < 1 {
                    Spacer()
                        .frame(height: 80)
                    HStack {
                        Spacer()
                        Text("아직 인연이 없어요")
                        Spacer()
                    }
                    Spacer()
                } else {
                    ForEach(viewModel.messageInfoList.indices, id: \.self) { index in
                        HStack(alignment: .center, spacing: 8) {
                            ZStack(alignment: .center) {
                                if index < viewModel.messageInfoList.count - 1 {
                                    DashedLine()
                                }
                                TimelinePoint()
                            }
                            .frame(width: 16)
                            MessageCard(messageInfo: viewModel.messageInfoList[index])
                        }
                        .padding(.bottom, 16)
                    }
                }
            }
            .padding()
        }
        .onAppear {
            viewModel.fetchRecentFeed()
        }
    }
}

Model과 ViewModel 추가 설명

Model은 데이터를 나타내는 구조체, 클래스, 또는 열거형입니다. Model은 앱의 데이터와 관련된 로직을 포함하며, 이는 사용자 인터페이스와 독립적입니다. Model은 일반적으로 앱의 비즈니스 로직, 데이터 구조, 네트워킹, 데이터베이스, 또는 API 호출과 관련된 코드를 포함합니다.

Model은 뷰와 뷰 모델 사이에서 데이터를 주고받기 위한 중간 역할을 합니다. 뷰 모델은 Model을 사용하여 데이터를 가져오고 처리한 후, 뷰에게 제공합니다. 뷰 모델이 데이터를 변경하면 Model도 업데이트됩니다.

예를 들어, MessageInfo 구조체는 Model의 일부로 볼 수 있습니다. MessageInfo는 데이터를 나타내는 속성들을 가지고 있으며, 뷰 모델에서 데이터를 가져와 뷰에게 전달합니다.

struct MessageInfo {
    var profileImage: String
    var stack: Int
    var nickname: String
    var contents: String
    var place: String?
    var hour: Int?
    var hasMultiple: Bool
    let cardColor: CardColor
}

TimelineViewModel에서 Model인 MessageInfo를 사용하여 뷰에게 데이터를 제공하고 있습니다. fetchRecentFeed() 함수를 통해 API 호출을 통해 데이터를 가져오고, 이를 MessageInfo 구조체로 변환하여 뷰에게 전달합니다.

class TimelineViewModel: ObservableObject {
    @Published var messageInfoList: [MessageInfo] = []

    // ...
    func fetchRecentFeed() {
        // ...
    }
}

이 예제에서 Model은 MessageInfo 구조체로 구성되어 있으며, TimelineViewModel이 이를 사용하여 데이터를 처리하고 뷰에게 전달합니다.


요약

ObservableObject 프로토콜로 ViewModel이나 전역저장소를 생성할 수 있고,
ViewModel을 @StateObject로 인스턴스를 생성해서 가져오고, @ObservedObject로 인스턴스를 공유할 수 있다.
이외, @EnvironmentObject로 생성한 인스턴스는 전역에서 값을 공유한다.

이를 활용하면, Model-View-ViewModel 디자인패턴으로 SwiftUI를 구성할 수 있다.

결론! 이 코드, 참 맛있다.

profile
iOS 개발자 지망생

0개의 댓글