포스트

Async/Await (4)

MVVM 디자인 패턴 적용하기

Async_Await (2)에서 했던 프로젝트를 이어서 진행한다.

구현할 매커니즘에 대해 간략하게 표현하면 다음과 같다

example31 drawio

Webservice 구현

Webservice 클래스 파일을 하나 만들어 준다.

그리고 기존에 구현했었던 getDate 함수를 옮겨준다.

1
2
3
4
5
6
7
8
9
10
11
12
class Webservice {
    
    private func getDate() async throws -> CurrentDate? {
        
        guard let url = URL(string: "https://ember-sparkly-rule.glitch.me/current-date") else {
            fatalError("URL is incorrect")
        }
        
        let (data, _) = try await URLSession.shared.data(from: url)
        return try? JSONDecoder().decode(CurrentDate.self, from: data)
    }
}

ViewModel 구현

이제 Webservice를 호출할 ViewModel을 구현해본다

또 새롭게 파일을 하나 만들어주고 이름은 CurrentDateListViewModel로 한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@MainActor
class CurrentDateListViewModel: ObservableObject {
    
    @Published var currentDates: [CurrentDateViewModel] = []
    
    func populateDates() async throws {
        do {
            let currentDate = try await Webservice().getDate()
            if let currentDate = currentDate {
                let currentDateViewModel = CurrentDateViewModel(currentDate: currentDate)
                    self.currentDates.append(currentDateViewModel)
            }
        } catch {
            print(error)
        }
    }
    
}

struct CurrentDateViewModel {
    let currentDate: CurrentDate
    
    var id: UUID {
        currentDate.id
    }
    
    var date: String {
        currentDate.date
    }
}
  1. ObservableObject로 선언
    • @Published 프로퍼티를 통해 View와 데이터를 연결.
    • 화면에 표시될 각 날짜는 CurrentDateViewModel 구조체로 표현.
  2. CurrentDateViewModel
    • Model(CurrentDate)을 기반으로 생성.
  3. 배열 업데이트
    • UI 업데이트를 위해 메인 스레드에서 배열 작업 수행.
    • iOS 15 이상에서는 @MainActor를 ViewModel에 선언해 모든 작업이 메인 스레드에서 실행되도록 설정.
      • DispatchQueue.main.async를 사용할 필요가 없어졌다.

ContentView에서 ViewModel 사용하기

  1. StateObject 생성
    • CurrentDateListViewModel의 인스턴스를 @StateObject로 생성하여 ContentView와 연결한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct ContentView: View {
    
    @StateObject private var currentDateListVM = CurrentDateListViewModel()
        
    var body: some View {
        NavigationView {
            List(currentDateListVM.currentDates) { currentDate in
                Text(currentDate.date)
            }.listStyle(.plain)
            
                .navigationTitle("Dates")
                .navigationBarItems(trailing: Button(action: {
                    // button action
                    async {
                        await currentDateListVM.populateDates()
                    }
                }, label: {
                    Image(systemName: "arrow.clockwise.circle")
                }))
                .task {
                    await currentDateListVM.populateDates()
                }
        }
    }
}

이렇게 ViewModel로 적용을 하다보면

CleanShot 2024-11-28 at 13 38 48

List에서 위와 같은 에러가 발생한다.

해당 에러는 SwiftUI의 List를 사용할 때 발생하며, List의 항목들이 고유하게 식별 가능하도록 Identifiable 프로토콜을 준수해야 한다는 요구 사항 때문이다.

이때 두가지 방법이 존재한다.

  1. struct CurrentDateViewModel에 Identifiable 프로토콜 채택
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct CurrentDateViewModel: Identifiable {
    private let currentDate: CurrentDate

    init(currentDate: CurrentDate) {
        self.currentDate = currentDate
    }

    var id: UUID {
        currentDate.id
    }

    var date: String {
        currentDate.date
    }
}

이런식으로 한다.

  1. List에서 식별자를 설정

이렇게 id를 적으면

아래와 같이 뜨는데, CleanShot 2024-11-28 at 13 43 31

보통은 식별을 id로 하기에 id를 해주자.

그리고 이런 KeyPath를 사용할때는 \ Backslash를 사용한다. CleanShot 2024-11-28 at 13 43 01

그리고 id를 해주면 된다.

그래서 위에 struct CurrentDateViewModel에서도 id를 만들어 두는 것.

참고하면 좋을 링크(Backslash)

  1. Youtube
  2. StackOverFlow

Identifiable Protocol?

Docs를 보면 설명이 있다.

여기서 나는 이게 포인트라고 생각한다.

  • Guaranteed always unique, like UUIDs.

다시 돌아와서 그다음엔 try가 필요하다는 에러가 나와서 try를 적어준다.

그리고 task있는곳에도 똑같이 try를 적고나니

CleanShot 2024-11-28 at 13 56 37

이런에러가 발생

throws가 선언된 비동기 함수를 호출하는 클로저가 throws를 지원하지 않는 함수 타입에 전달되었을 때 발생한다.

1
2
3
4
5
6
7
8
func populateDates() async throws {
        do {
            생략
         } catch {
             print(error)
         }
        
}

이미 위에서 throws를 적었는데도 불구하고 do ~ catch 를 통해 에러를 해결하기 때문에 발생

throws를 지워주자. 그래도 여전히 에러가 발생한다.

이젠 throw가 없으므로 에러를 호출한쪽으로 던지지 않으니 try도 다시 지워준다.

실행화면은 똑같으니 올리지 않는다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.