포스트

TourApp (6)

Api 적용하기

우선 json으로 만든 파일을 웹사이트에 올려 api처럼 가져오게 했다.

이후, Medium 글을 통해서 코드를 작성했다.

이글을 통해서 작성한것은 바로 Generic을 사용했다는 점이다. 이전에 Generic을 사용해본적이 없기에 이번에는 좀 사용하면서 내걸로 조금씩 만들고 싶었다.

ApiModel 만들기

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
31
32
33
34
35
36
37
38
39
struct ApiModel: Codable {
    let tours: [Tour]
}

enum NetworkError: Error {
    case badUrl
    case invalidRequest
    case badResponse
    case badStatus
    case failedToDecodeResponse
}

class ApiService {
    func downloadData<T: Codable>(fromURL: String) async -> T? {
        do {
            guard let url = URL(string: fromURL) else { throw NetworkError.badUrl }
            let (data, response) = try await URLSession.shared.data(from: url)
            guard let response = response as? HTTPURLResponse else { throw NetworkError.badResponse }
            guard response.statusCode >= 200 && response.statusCode < 300 else { throw NetworkError.badStatus }
            guard let decodedData = try? JSONDecoder().decode(ApiModel.self, from: data) else {
                throw NetworkError.failedToDecodeResponse
            }
            
            return decodedData.tours as? T
        } catch NetworkError.badUrl {
            print("There was an error creating the URL")
        } catch NetworkError.badResponse {
            print("Did not get a valid response")
        } catch NetworkError.badStatus {
            print("Did not get a 2xx status code from the response")
        } catch NetworkError.failedToDecodeResponse {
            print("Failed to decode response into the given type")
        } catch {
            print("An error occured downloading the data")
        }
        
        return nil
    }
}

여기서 하나 다른점이라면

1
2
3
guard let decodedData = try? JSONDecoder().decode(ApiModel.self, from: data) else {
                throw NetworkError.failedToDecodeResponse
            }

이 부분이다 참고글에서는 여기도 역시 T.self를 통해 Generic을 사용했지만, 내가만든 json의 구조에서는

1
2
3
4
5
6
7
8
9
"tours": [
        {
            "title": "해운대",
            "imageUrl": "https://www.visitbusan.net/uploadImgs/files/cntnts/20191229153531987_oen",
            "description": "해운대해수욕장은 대한민국 부산광역시 해운대구 중동과 우동에 걸쳐서 위치한 대한민국 최대규모의 해수욕장이다.\n모래사장의 총면적은 120,000m², 길이는 1.5 km,\n폭은 70m ~ 90m이다",
            "address": "부산광역시 해운대구 해운대해변로 280",
            "latitude": 35.1594965,
            "longitude": 129.162576,
            "resList": [

이런식으로 tours라는 녀석으로 시작해서 담고있기에 Generic을 사용하려면 애초에 json을 구성할때 tours를 뺐어야했다.

하지만 이미 업로드 하기도 해서 저기만 ApiModel.self를 사용했다.

ApiViewModel 만들기

@MainActor 항상 Main Queue에서 실행이 된다. 즉 Uikit에서 사용했던 DispatchQueue.main이라고 생각하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Foundation

@MainActor class ApiViewModel: ObservableObject {
    @Published var apiData = [Tour]()
    
    init() {
        Task {
            await fetchData()
        }
    }
    
    func fetchData() async {
        let url = "https://run.mocky.io/v3/42391865-6e96-4db3-9f68-1e2970796cad"
        guard let downloadedData: [Tour] = await ApiService().downloadData(fromURL: url) else { return }
        apiData = downloadedData
    }
}

특징DispatchQueue.main@MainActor
메인 스레드에서의 실행 요청명시적으로 DispatchQueue.main.async 사용자동으로 메인 스레드에서 실행
적용 범위특정 코드 블록메서드, 프로퍼티, 클래스 전체
코드 가독성복잡할 수 있음가독성이 높아짐
비동기 메서드와의 호환성추가로 async 블록 필요async 메서드에서도 안전하게 실행

즉,

  • @MainActor를 사용하면, 명시적으로 DispatchQueue.main.async를 사용할 필요가 없다.
  • @MainActor는 메서드나 클래스 전체에서 메인 스레드 실행을 보장하기 때문에, 코드가 더 간결하고 안전해진다.
  • Swift의 최신 비동기 API(async/await)와 잘 어울리며, UI 업데이트 코드에서 더 많이 사용된다.

init을 사용한건, 여기서 내가 만든 json은 이미 내용이 정해져있고, 양이 방대하지도 않고 페이징이필요없기에 한번 로드하면 끝이라 init으로 한번 로드했을때 가져오게 했다.

그게아니라 View가 다시 로드될때마다 사용하고 싶다면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ObservedObject var json = loadJsonModel()
@StateObject var vm = ApiViewModel()

NavigationStack {
            Text("관광 고고")
                List {
                    ForEach(vm.apiData, id: \.self) { tour in
                        NavigationLink(value: tour) {
                            CellView(title: tour.title, imageUrl: tour.imageURL)
                        }
                    }
                }
            .navigationDestination(for: Tour.self) { model in
                DetailView(title: model.title, imageUrl: model.imageURL, description: model.description, address: model.address, coordinate: CLLocationCoordinate2D(latitude: model.latitude, longitude: model.longitude), shopList: model.resList, cameraPosition: .camera(MapCamera(centerCoordinate: CLLocationCoordinate2D(latitude: model.latitude, longitude: model.longitude), distance: 500, heading: 90, pitch: 80)))
            }
        }
        .onAppear {
            if vm.apiData.isEmpty {
                Task {
                    await vm.fetchData()
                }
            }
        }

onAppear를 통해서 함수를 호출하면 된다.

@ObservedObject vs @StateObject 비교는 새롭게 글을 작성해야 할듯하다.

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