apple/SwiftUI & Combine

[Combine] Networking(feat. RestAPI)

lgvv 2022. 6. 11. 16:23

Networking(feat. RestAPI)

Combine을 활용해서 쉽게 서버와 통신을 처리할 수 있습니다.

이 글의 제일 아래 부분에서는 MVVM에 대한 고찰도 들어 있습니다.

 

난이도는 총 3가지로 나뉘어 있습니다. 파일을 확인해주세요!

이 포스팅에서는 Refactoring(상)과 Advanced Model에 대해서만 설명합니다.

자세한 코드는 아래 첨부 파일을 확인해주세요!!!

CombineNetworking.zip
0.07MB

🥕 목차 🥕

1. Refactoring (상)

2. MVVM에 대한 고찰(Combine 한달차,, 2022.06.11)

 

🟠 모델

//
//  UserModel.swift
//  CombineNetworking
//
//  Created by Hamlit Jason on 2022/06/10.
//

//   let user = try? newJSONDecoder().decode(User.self, from: jsonData)
import Foundation

// MARK: - UserElement
struct UserElement: Codable, Hashable {
    let id: Int
    let name, username, email: String
    let address: Address
    let phone, website: String
    let company: Company
    
    static func == (lhs: UserElement, rhs: UserElement) -> Bool {
        return lhs.name == rhs.name && lhs.username == rhs.username
    }
}

// MARK: - Address
struct Address: Codable, Hashable {
    let street, suite, city, zipcode: String
    let geo: Geo
    
    static func == (lhs: Address, rhs: Address) -> Bool {
        return lhs.street == rhs.street
    }
}

// MARK: - Geo
struct Geo: Codable, Hashable {
    let lat, lng: String
    
}

// MARK: - Company
struct Company: Codable, Hashable {
    let name, catchPhrase, bs: String
    
}

typealias User = [UserElement]

모델에서 주목해야 하는 부분은 Hashable입니다. 이 부분을 채택해야지 비교가 가능하기 때문에 Hashable을 상속받고 구현합니다. == 메소드 안에서 구현하는데, 단순한 예제라서 정말 하나의 속성만 맞으면 같다고 판단하게 설정하였습니다.

 

 

🟠 뷰

//
//  RefactoringView.swift
//  CombineNetworking
//
//  Created by Hamlit Jason on 2022/06/11.
//

import SwiftUI

struct RefactoringView: View {
    @EnvironmentObject var engine: RefactoringEngine
    
    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    Image(systemName: "magnifyingglass")
                        .foregroundColor(Color.green)
                    
                    TextField("", text: $engine.typing, onCommit: {
                        engine.input = engine.typing
                    })
                        
                }
                .padding(.horizontal, 20)
                .frame(height: 40)
                .background(
                    RoundedRectangle(cornerRadius: 20)
                        .stroke(Color.green, lineWidth: 1)
                )
                .padding([.horizontal, .top], 16)
                .padding(.bottom, 10)
                
                Spacer()
                
                List {
                    ForEach(engine.users, id: \.self) { user in
                        Text("id필터링 에시!: \(user.id)")
                    }
                    .listStyle(.plain)
                }
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        Text("리팩토링엔진 장착!")
                    }
                }
            }
            
            
        }
    }
}

view 부분입니다. 특별한 점은 없습니다. 

다만 네비게이션뷰를 설정할 때 navigationBarTitleDisplayMode를 지정해주지 않으면, 우리가 원한 값보다 큰 영역으로 네비게이션 뷰를 차지해서 원하는 결과가 아닐 수 있습니다.

 

🟠 엔진

//
//  RefactoringEngine.swift
//  CombineNetworking
//
//  Created by Hamlit Jason on 2022/06/11.
//
// 고급 json: https://jsonplaceholder.typicode.com/users
import Combine
import SwiftUI

class RefactoringEngine: ObservableObject {
    static let urlString = "https://jsonplaceholder.typicode.com/users"
    
    @Published var users: User = []
    var cancleables = Set<AnyCancellable>()
    
    @Published var typing: String = ""
    @Published var input: String = "" {
        didSet {
            send(filterId: Int(input))
        }
    }
    
    init() {
        send()
    }
    
    func send(filterId: Int? = nil) {
        guard let url = URL(string: RefactoringEngine.urlString) else { return }
        
        URLSession.shared.dataTaskPublisher(for: url)
            .subscribe(on: DispatchQueue.global(qos: .background))
            .receive(on: DispatchQueue.main)
            .tryMap(handleOutput)
            .decode(type: User.self, decoder: JSONDecoder())
            .map { users in
                return users.filter { user in
                    user.id != filterId
                }
            }
            .sink { completion in
                print("✅ completion \(completion)")
            } receiveValue: { [weak self] responseElements in
                print("✅ responseElements \(responseElements)")
                self?.users = responseElements
            }
            .store(in: &cancleables)
        
    }
    
    func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data {
        print("✅ \(#function)")
        guard
            let response = output.response as? HTTPURLResponse,
            (200..<300) ~= response.statusCode else {
                print("✅ \(#function) guard에 걸렸습니다.")
                throw URLError(.badServerResponse)
            }
        
        print("✅ \(#function) guard를 탈출했습니다.")
        return output.data
    }
}

여기도서도 그리 어렵지 않게 처리하고 있습니다. 해당 코드는 텍스트에서 원하는 조건값을 입력하면 필터링이 가능하게끔 지원해줍니다.

기존에는 클로저를 통해서 작성해서 위의 코드보다 조금 더 긴 코드가 복잡하게 다가왔습니다.

하지만 위의 코드를 통해 정말 깔끔하게 처리할 수 있습니다.

 

 

 

2. MVVM에 대한 고찰(Combine 한달차,, 2022.06.11)

 

SwiftUI + Combine을 공부할 때 UIKit + RxSwift를 사용하니까 UI의 구성 방식이던가, delegate와 같은 부분이 어려웠다. 이해가 깊어질수록 어려운 점이 더 생겼는데, 'MVVM이 잘 어울리는가?' 이다.

 

파일을 열어보면 알겠지만, 예를들어 EasyView가 있다고 하면 EasyViewModel로 네이밍하면 좋을 파일을 EasyEngine으로 네이밍 했다. 이게 더 직관적이고, 잘 어울린달까..?

 

내가 진행하는 프로젝트의 app단을 UIKit에서 SwiftUI로 전환하였는데, viewModel이라는 네이밍보다 해당 기능에 더 알맞는 네이밍을 채택해서 사용하고 있다.