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 |