본문 바로가기
Dev/iOS

[SwiftUI/Combine ]SwiftUI Combine을 활용한 Network 매니저 클래스 활용

by steady.dev 2022. 3. 25.

Combine 프레임워크는 비동기적인 작업을 처리하는 데 매우 효과적인 도구입니다.

 MVVM 패턴에서의 각 구성 요소는 다음과 같습니다.

  • Model: 앱의 데이터를 나타냅니다.
  • View: 사용자에게 데이터를 표시합니다.
  • ViewModel: Model과 View 간의 중개자 역할을 합니다.

 

 

1. Netwokr Model Class 생성

네트워크 API 클래스는 Model에 해당하며, ViewModel에서 호출됩니다. 따라서, API 클래스에서는 네트워크 요청을 만들고 응답을 처리하는 로직을 작성해야 합니다.

다음은 Swift Combine과 MVVM 패턴을 사용하여 네트워크 API 클래스를 만드는 예시입니다.

 

class NetworkService {
    
    enum NetworkError: Error {
        case invalidURL
        case requestFailed
        case unknown
    }
    
    private let baseURL = URL(string: "https://api.example.com")!
    private let session: URLSession
    
    init(session: URLSession = .shared) {
        self.session = session
    }
    
    func fetchData<T: Decodable>(from endpoint: String) -> AnyPublisher<T, Error> {
        guard let url = URL(string: endpoint, relativeTo: baseURL) else {
            return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher()
        }
        
        return session.dataTaskPublisher(for: url)
            .mapError { error -> Error in
                if let urlError = error as? URLError {
                    return urlError
                } else {
                    return NetworkError.unknown
                }
            }
            .flatMap { data, response -> AnyPublisher<T, Error> in
                guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                    return Fail(error: NetworkError.requestFailed).eraseToAnyPublisher()
                }
                
                let decoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                
                return Just(data)
                    .decode(type: T.self, decoder: decoder)
                    .mapError { error -> Error in
                        return error
                    }
                    .eraseToAnyPublisher()
            }
            .eraseToAnyPublisher()
    }
}

 

위 코드에서는 Combine 프레임워크를 사용하여 fetchData 메서드를 작성합니다.

이 메서드는 endpoint 파라미터를 통해 호출할 API의 경로를 받아들이고, 해당 경로에서 데이터를 가져옵니다. 가져온 데이터는 T 타입으로 디코딩되며, 성공하면 AnyPublisher<T, Error>를 반환합니다. 만약 에러가 발생하면, 해당 에러를 적절한 Error 타입으로 변환하여 반환합니다.

 

 

2. Network Model class를 통한 ViewModel 비지니스 로직 구현

이제 ViewModel에서는 위 클래스의 fetchData 메서드를 호출하여 데이터를 가져올 수 있습니다. View에서는 ViewModel과 바인딩된 데이터를 사용하여 UI를 업데이트합니다.

 

이제 Swift Combine 프레임워크와 MVVM 패턴을 사용하여 NetworkService 클래스를 만들었으므로 ViewModel에서 이를 사용하여 API에서 데이터를 가져올 수 있습니다.

다음은 방금 만든 NetworkService 클래스를 사용하여 REST API에서 사용자 목록을 가져오는 ViewModel 예제입니다

 

class UserListViewModel: ObservableObject {
    
    private let networkService: NetworkService
    private var cancellables = Set<AnyCancellable>()
    
    @Published var users = [User]()
    
    init(networkService: NetworkService) {
        self.networkService = networkService
    }
    
    func fetchUsers() {
        networkService.fetchData(from: "/users")
            .sink(receiveCompletion: { result in
                switch result {
                case .failure(let error):
                    print("Error fetching users: \(error.localizedDescription)")
                case .finished:
                    break
                }
            }, receiveValue: { users in
                self.users = users
            })
            .store(in: &cancellables)
    }
}

이 ViewModel에는 게시되고 View에서 관찰할 수 있는 사용자 속성이 있습니다. 또한 NetworkService 클래스를 사용하여 API에서 사용자 목록을 가져오고 데이터가 수신되면 사용자 속성을 업데이트하는 fetchUsers 메서드를 작성합니다.

sink operator를 사용하여 fetchData 메서드가 반환한 Publisher를 구독합니다.

 

sink operator는 receiveCompletion과 receiveValue라는 두 가지 클로저를 사용합니다. receiveCompletion 클로저는 퍼블리셔가 값 전송을 완료했거나 오류가 발생했을 때 호출되고, receiveValue 클로저는 퍼블리셔가 새 값을 전송할 때마다 호출됩니다.

sink operator가 반환한 취소 가능 항목을 취소 가능 속성에 저장하여 ViewModel이 할당 해제된 경우 네트워크 요청을 취소할 수 있도록 합니다.

 

 

3. View 구현

그런 다음 실제 사용자에게 보여줄 View에서 UserListViewModel의 인스턴스를 생성하고 fetchUsers 메서드를 호출하여 사용자 목록을 가져올 수 있습니다.

struct UserListView: View {
    
    @ObservedObject var viewModel: UserListViewModel
    
    var body: some View {
        List(viewModel.users, id: \.id) { user in
            Text(user.name)
        }
        .onAppear {
            viewModel.fetchUsers()
        }
    }
}

여기서는 @ObservedObject 프로퍼티 래퍼를 사용하여 UserListViewModel 인스턴스의 변경 사항을 관찰합니다. 또한 onAppear 수정자를 사용하여 뷰가 표시될 때 fetchUsers 메서드를 호출합니다.

 

댓글