포스트

10주차 과제 (2)

SearchBar Publisher 생성

CollectionView, TableView의 Delegata, DataSource를 어떻게 해야할지 고민 하기전에,

우선 SearchBar에 대한 부분은 먼저 끝내는게 좋다고 판단했다.

Publisher를 하나 만들어준다.

사이트를 참고하여 searchBar에 publisher를 연결하여 구현해본다.

우선 textPublisher를 사용하기 위해 extension을 하나 만들어 주고

1
2
3
4
5
6
7
8
extension UITextField {
    var textPublisher: AnyPublisher<String, Never> {
        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: self)
            .map { ($0.object as? UITextField)?.text  ?? "" }
            .eraseToAnyPublisher()
    }
}

SearchView에서 observe 함수를 하나 만들어서

1
2
3
4
5
private func observe() {
        searchBar.searchTextField.textPublisher.sink { value in
            print(value)
        }.store(in: &cancellables)
    }

text값을 출력하게 해보았다.

바로바로 출력이 되는걸 확인했다.

May-05-2024 05-09-01

이젠 이렇게 입력되는 값을 api로 넘기기 전에 실시간으로 호출을 하면 트래픽도 있고 해서 좋지 않기에

입력하고 1초 뒤에 넘어가게 하는게 좋아보인다.

debounce는 우리가 정한 시간뒤에 값을 넘기는게 가능하다.

1
2
3
4
5
6
7
private func observe() {
        searchBar.searchTextField.textPublisher
            .debounce(for: 1, scheduler: RunLoop.main) // added
            .sink { value in
            print(value)
        }.store(in: &cancellables)
    }

그리고 main thread에서 작업하게 해두었다.

이젠 값을 바로바로 넘기는게 아니라 유져가 입력하고 1초를 기다렸다가 넘기게 된다.

May-05-2024 05-15-28

이렇게 1초뒤에 입력이됨.

모델링

API Data에 관한 모델링은 다음과 같이 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct BookModel: Codable {
    
    let documents: [Document]
    
}

struct Document: Codable {
    
    let authors: String
    let contents: String
    let price: Int
    let title: String
    let thumbnail: String
    let salePrice: Int
    
    enum Codingkeys: String, CodingKey {
        case salePrice = "sale_price"
    }
    
}

API 호출 기능 구현.

1
2
3
4
5
6
7
private func observe() {
        searchBar.searchTextField.textPublisher
            .debounce(for: 1, scheduler: RunLoop.main)
            .sink { [weak self] value in
                self?.searchBarSubject.send(value) // added
        }.store(in: &cancellables)
    }

우선 값을 입력한걸 searchbarSubject를 통해 보낸다.

여기까지 해두고 NetworkManager하나 만들어준다.

우선 어떻게 값이 전달이 되어야할지 (ex: Header, Query)를 확인하기 위해 Postman을 사용하여 확인을 먼저한다.

CleanShot 2024-05-05 at 11 10 03@2x

혹시나해서 title도 일종의 파라미터일까 했지만 아니다.

즉 Docs에 있는 그대로, Header / Query가 둘다 필요하다는 뜻이 된다.

여기서도 해보다가 알게된 두가지 방법이 있지만, 보편적인 방법을 사용하는걸로. 여기서 독특한 점이라면 urlsession에서 publisher를 사용한다는 것이다.

1
func fetchRequest(queryValue: String) -> AnyPublisher<[Document], Error> { }

우선 이렇게 틀을 잡고 시작한다.

return type은 publisher가 되게한다. 이때 나가는게 [Document]로 된다.

Escaping Closure랑 비슷하게 넘어가는걸로 이해하면 될듯.

그리고 내용을 적어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let urlString = "https://dapi.kakao.com/v3/search/book?target=title"
let headers = ["Authorization" : "KakaoAK \(Secret.apikey)"]
        
var urlComponent = URLComponents(string: urlString)
urlComponent?.queryItems?.append(URLQueryItem(name: "query", value: queryValue))
        
guard let url = urlComponent?.url else {
    return Fail(error: URLError(.badURL)).eraseToAnyPublisher() // Error 리턴
}
        
var request = URLRequest(url: url)
request.allHTTPHeaderFields = headers
let session = URLSession(configuration: .default)

여기까지는 뭐 우리가 하던 방식대로 진행하는 부분이니 크게 문제가 없다.

그다음 리턴에서 차이가 나게된다.

1
2
3
4
return session.dataTaskPublisher(for: request)
        .map(\.data)
        .decode(type: [Document].self, decoder: JSONDecoder())
        .eraseToAnyPublisher()

우선 이렇게 해두었다.

받아온 data를 decoding한다.

그리고 BookVM을 만들고 해당 기능을 호출할 함수를 하나 만들어주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private var cancellables = Set<AnyCancellable>()
    
    func callRequest() {
        
        NetworkManager.shared.fetchRequest(queryValue: "미움받을 용기").sink { completion in
            switch completion {
            case .finished:
                print("success")
            case .failure(let error):
                print(error)
            }
        } receiveValue: { model in
            print(model.documents)
        }.store(in: &cancellables)

    }

실행하니 에러가 난다. 처음부터 documents를 다 들고와야하는건가 싶다.

다시 바꿔주고, 실행하니 authors에서 typemismatch가 발생

찾아보니 이녀석 string 배열이다.

Model 재수정

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

struct BookModel: Codable {
    
    let documents: [Document]
    
}

struct Document: Codable {
    
    let authors: [String] // modified
    let contents: String
    let price: Int
    let title: String
    let thumbnail: String
    let salePrice: Int? // modified
    
    enum Codingkeys: String, CodingKey {
        case salePrice = "sale_price"
    }
    
}

salePrice에 optional? 할인을 할 수도 있고, 안할수도있어서…

아래는 NetworkManager에 대한 코드

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
class NetworkManager {
    
    static let shared = NetworkManager()
    
    private var cancellables = Set<AnyCancellable>()
    
    func fetchRequest(queryValue: String) -> AnyPublisher<BookModel, Error> {
        
        let urlString = "https://dapi.kakao.com/v3/search/book?target=title"
        let headers = ["Authorization" : "KakaoAK \(Secret.apikey)"]
        
        var urlComponent = URLComponents(string: urlString)
        urlComponent?.queryItems?.append(URLQueryItem(name: "query", value: queryValue))
        
        guard let url = urlComponent?.url else {
            return Fail(error: URLError(.badURL)).eraseToAnyPublisher() // Error 리턴
        }
        
        var request = URLRequest(url: url)
        request.allHTTPHeaderFields = headers
        let session = URLSession(configuration: .default)
        return session.dataTaskPublisher(for: request)
            .print() // 과정 확인
            .map(\.data)
            .decode(type: BookModel.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

호출 성공.

CleanShot 2024-05-05 at 13 51 43@2x

중복이 뭐이리 많나 했더니/

여러 군데에서 검색을 해서 가져오는가보다.

SearchBar의 text 값을 vm으로 넘겨서 api처리.

우선 vm으로 가서

searchView에서 한 데이터가 vm까지 통신이 되는지를 확인해보는게 중요하다.

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
//vm
struct Input {
        let searchPublisher: AnyPublisher<String, Never>
    }

private var cancellables = Set<AnyCancellable>()
    
func transform(input: Input) {
        
        input.searchPublisher.sink { [weak self] value in
            self?.callRequest(query: value)
        }.store(in: &cancellables)
    
    }
    
    
func callRequest(query: String) {
        
        NetworkManager.shared.fetchRequest(queryValue: query).sink { completion in
            switch completion {
            case .finished:
                print("success")
            case .failure(let error):
                print(error)
            }
        } receiveValue: { [weak self] model in
            self?.document = model.documents
            print(model.documents)
        }.store(in: &cancellables)
    }

//vc
 override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        layout()
        bookVM.transform(input: BookVM.Input(searchPublisher: searchView.valuePublisher))
    }

우선은 다음과 같이 구성을 했다.

이렇게 하면 searchbar에서 입력한 텍스트가 vm으로 가서 transform으로 간다음, 다시 callrequest로 가서 api와 통신을 한다.

즉 vc입장에선 뭐 크게 할게 없다.

기존에 mvc였다면 vc에서 escaping closure나, delegate를 통해 처리한뒤 그걸 ui로 띄우는데, 아직 ui는 넘기지 못했지만,

이것만으로도 엄청나게 vc입장에선 자유로워진다.

확실히 기존에 하던 방식에서 새롭게 하니 오래걸리기도하고, 그렇다.

May-05-2024 15-19-57

그래도 나중에 뭘할때 이걸 기반으로 할 수 있을것같다.

MVVM도 희미하게 보이기 시작한다.

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