1. NetworkService
우선 네트워크 처리를 할 수 있는 객체를 생성해야 합니다. 네트워크 처리를 위해 필요한 것으로 URLSession과 URLRequest이 있습니다.
URLRequest
URLRequest는 네트워킹하는 곳에서 매번 생성하기에는 귀찮은 작업입니다. NetworkMethod도 String으로 입력하므로 오탈자가 발생하는 등의 문제가 생길 수도 있습니다. 따라서 따로 객체로 분리하면 더 편하게 사용할 수 있습니다. 아래 코드에서 body는 보통 post할 때 서버로 보낼 데이터를 저장합니다.
enum NetworkMethod: String {
case get
case post
case put
case patch
case delete
}
struct RequestBuilder {
let url: URL?
let method: NetworkMethod = .get
let body: Data?
let headers: [String: String]?
func create() -> URLRequest? {
guard let url = url else { return nil }
var request = URLRequest(url: url)
request.httpMethod = method.rawValue.uppercased()
if let body = body {
request.httpBody = body
}
if let headers = headers {
request.allHTTPHeaderFields = headers
}
return request
}
}
URLSession
세션은 상황에 따라 달라질 수 있으므로 외부에서 주입받도록 합니다. 부스트코스: URLSession과 URLSessionDataTask
final class NetworkService {
private let session: URLSession
init(session: URLSession) {
self.session = session
}
}
Request Method
Combine을 이용하면 코드가 더 단순해집니다. 대신 AnyPublisher<Data, NetworkError>로 반환하기 위해 .eraseToAnyPublisher() 메소드를 추가해야 합니다.
func request(request: URLRequest) -> AnyPublisher<Data, NetworkError> {
return session.dataTaskPublisher(for: request)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidRequest
}
return data
}
.mapError { error -> NetworkError in
.unknownError(message: error.localizedDescription)
}
.eraseToAnyPublisher()
}
NetworkError
네트워크 에러를 처리할 수 있는 enum을 만들었습니다.
enum NetworkError: Error {
case invalidRequest
case unknownError(message: String)
}
전체 코드
import Foundation
import Combine
final class NetworkService {
enum NetworkError: Error {
case invalidRequest
case unknownError(message: String)
}
private let session: URLSession
init(session: URLSession) {
self.session = session
}
func request(request: URLRequest) -> AnyPublisher<Data, NetworkError> {
return session.dataTaskPublisher(for: request)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidRequest
}
return data
}
.mapError { error -> NetworkError in
.unknownError(message: error.localizedDescription)
}
.eraseToAnyPublisher()
}
deinit {
session.invalidateAndCancel()
}
}
2. ImageLoader
ObservableObject
지속적으로 상태 변화를 감지하기 위해 이미지는 @Published property wrapper로 감싸주고 해당 객체는 ObservableObject 프로토콜을 채택합니다.
class URLImageLoader: ObservableObject {
@Published var image = UIImage(named: "logo")
}
AnyCancellable
그 다음으로 위에서 만든 네트워크 객체를 생성하고 fetch method를 작성합니다.
네트워크 객체의 request 메소드를 호출했을 때에는 sink로 결과를 받을 수 있도록 합니다.
데이터 처리는 뷰와 관련된 것이므로 DispatchQueue.main.async으로 감싸서 작업할 수 있도록 합니다.
마지막으로 sink의 반환값을 cancellables에 저장합니다.
private let network = NetworkService(session: URLSession.shared)
private var cancellables = Set<AnyCancellable>()
func fetch(urlString: String?) {
guard let urlString = urlString else { return }
let url = URL(string: urlString)
let urlRequest = RequestBuilder(url: url,
body: nil,
headers: nil).create()
guard let request = urlRequest else { return }
network.request(request: request)
.sink { result in
switch result {
case .failure(let error):
print(error)
case .finished:
print("success")
}
} receiveValue: { [weak self] data in
DispatchQueue.main.async {
self?.image = UIImage(data: data)
}
}
.store(in: &cancellables)
}
AnyCancellable Cancel
마지막으로 객체가 제거될 때 모든 AnyCancellable을 cancel할 수 있도록 합니다.
deinit {
cancellables.forEach { $0.cancel() }
}
전체 코드
import SwiftUI
import Combine
class URLImageLoader: ObservableObject {
@Published var image = UIImage(named: "logo")
private let network = NetworkService(session: URLSession.shared)
private var cancellables = Set<AnyCancellable>()
func fetch(urlString: String?) {
guard let urlString = urlString else { return }
let url = URL(string: urlString)
let urlRequest = RequestBuilder(url: url,
body: nil,
headers: nil).create()
guard let request = urlRequest else { return }
network.request(request: request)
.sink { result in
switch result {
case .failure(let error):
print(error)
case .finished:
print("success")
}
} receiveValue: { [weak self] data in
DispatchQueue.main.async {
self?.image = UIImage(data: data)
}
}
.store(in: &cancellables)
}
deinit {
cancellables.forEach { $0.cancel() }
}
}
3. URLImage
URL만으로 Image를 생성할 수 있도록 URLImage 객체를 만들어 줍니다.
onAppear
View가 화면에 표시될 때 이미지를 로드할 수 있도록 다음과 같은 코드를 추가해줍니다.
view lifecycle events: onAppear and onDisappear
var body: some View {
content
.onAppear {
imageLoader.fetch(urlString: urlString)
}
}
View와 분기
SwiftUI에서 if문과 같이 분기를 나누기 위해서는 Group으로 감싸주거나 해당 뷰 자체를 ViewBuilder로 만들어줘야합니다.
private var content: some View {
Group {
if let image = imageLoader.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
ActivityIndicatorView()
.padding()
}
}
}
전체 코드
import SwiftUI
import Combine
struct URLImage: View {
@StateObject private var imageLoader = URLImageLoader()
private let urlString: String?
init(urlString: String?) {
self.urlString = urlString
}
var body: some View {
content
.onAppear {
imageLoader.fetch(urlString: urlString)
}
}
private var content: some View {
Group {
if let image = imageLoader.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
ActivityIndicatorView()
.padding()
}
}
}
}
4. 참고
'iOS > SwiftUI' 카테고리의 다른 글
| SwiftUI) 사용자 이벤트 수집 및 Alert로 확인 (0) | 2020.12.16 |
|---|---|
| SwiftUI) NavigationLink와 Memory Leak (2) | 2020.12.05 |
| SwiftUI) ViewBuilder 와 guard let (1) | 2020.12.03 |
| SwiftUI) ObservableObject와 상속 (1) | 2020.12.02 |
| SwiftUI) MVVM과 Combine (0) | 2020.11.24 |