포스트

10주차 과제 (8)

이제 무한스크롤만하면 할건 다했다, 그리고 VM에서 alert 구현하면 끝날것같다.

무한스크롤 기능 추가.

CleanShot 2024-05-08 at 16 50 28@2x

보아하니 meta가 관리하는걸로 보인다.

BookModel에 meta를 추가

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

struct Meta: Codable { //added
    
    var isEnd: Bool
    var pageCount: Int
    var totalCount: Int
    
    enum Codingkeys: String, CodingKey {
        
        case isEnd = "is_end"
        case pageCount = "pageable_count"
        case totalCount = "total_count"
    }
}

이게 추가되자마자 검색이 먹지 않는다 왜냐하면

위에는 바꿨지만 meta가 배열안에 있는걸로 봐버렸다.

[Meta] -> Meta로 수정한다

그리고 테스트를하는데 nil값이 확인되는것같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Meta: Codable {
    
    var isEnd: Bool? // modified
    var pageCount: Int? // modified
    var totalCount: Int? // modified
    
    enum Codingkeys: String, CodingKey {
        
        case isEnd = "is_end"
        case pageCount = "pageable_count"
        case totalCount = "total_count"
    }
}

이렇게 수정을 해주니 보인다.

도대체 쿼리가 어디있나 했더니

CleanShot 2024-05-08 at 17 09 22@2x

위에 있었다.

그냥 예시를 보고 하다보니 이렇게 된 결과였다.

이젠 url도 docs의 예시가 아닌 진짜 다른걸로 사용할때가 되었다.

하지만 여전히 meta의 값이 nil이 나온다.

주소가 잘못된걸까?

CleanShot 2024-05-08 at 23 43 22@2x

아직도 나타나지 않는 meta 너란녀석.

문제를 찾았다 codingKey의 문제였다.

분명히 틀린게 없는데 왜 잘못되었는지 모르겠다.

무튼 다시적으니 제대로 인식이 된다.

willDisplay를 쓰려고하니 검색결과가 나오자마자 함수가 호출이 되버린다. 그말은 즉 검색 하자마자 스크롤이 된다는뜻.

우선 새롭게 함수를 짜서 테스트를 해보려한다.

그에따라 Publisher또 한 새롭게 준비.

Model 그자체라 Initializeing이 필요하여 이렇게 부여를 해둔 상태.

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
func fetchTotalRequest(queryValue: String) {
        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))
        urlComponent?.queryItems?.append(URLQueryItem(name: "page", value: "1"))
        
        guard let url = urlComponent?.url else {
            return
        }
        
        var request = URLRequest(url: url)
        request.allHTTPHeaderFields = headers
        request.httpMethod = "GET"
        request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
        let session = URLSession(configuration: .default)
        
        session.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: BookModel.self, decoder: JSONDecoder())
            .replaceError(with: totalDocumnet) // modified → 에러가 발생할땐 아무것도 없는 init 상태의 값 리턴. 
            .assign(to: \.totalDocumnet, on: self)
            .store(in: &cancellables)
    }

이렇게 세팅을 하고 willDisplay를 구현한다.

1
2
3
4
5
6
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if indexPath.section == 0 && indexPath.row == searchVM.document.count - 1 { // 마지막에 도달했을때
            
        }
        
    }

여기서 함수를 그대로 호출을 하면 될듯하다.

구성

1
2
3
4
5
6
7
8
9
10
11
12
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if indexPath.section == 0 && indexPath.row == searchVM.document.count - 1 { // 마지막에 도달했을때
            page += 1
            searchVM.transform(input: SearchVM.Input(searchPublisher: searchView.valuePublisher),page: page)
            searchVM.$document
                .receive(on: DispatchQueue.main)
                .sink { [weak self] _ in
                    self?.resultView.tableView.reloadData()
                }.store(in: &cancellables)
        }
        
    }

page를 받아야 하므로 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
func fetchTotalRequest(queryValue: String, page: Int) { // modified
        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))
        urlComponent?.queryItems?.append(URLQueryItem(name: "page", value: page.stringValue)) // added
        
        guard let url = urlComponent?.url else {
            return
        }
        
        var request = URLRequest(url: url)
        request.allHTTPHeaderFields = headers
        request.httpMethod = "GET"
        
        let session = URLSession(configuration: .default)
        session.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: BookModel.self, decoder: JSONDecoder())
            .map { model in
                if model.meta.isEnd == false {
                    return model.documents
                } else {
                    return []
                }
            }
            .replaceError(with: []) // 에러가 발생할땐 아무것도 없는 init 상태의 값 리턴.
            .assign(to: \.document, on: self)
            .store(in: &cancellables)
    }

실행하니 엄청나게 빠르게 로딩이 되어버린다. 뭔가 로직 개선이 필요하다.

현재 의심이 되는건 searchVM.document.count - 1 여기파트이다.

역시나 문제였다. searchVM의 count가 갱신이 되지않고 10개인채로 유지가 되었기에 무한 로딩이 발생.

아무래도 vm에서 fetchRequest부분을 수정할 때가 된듯 하다.

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
func fetchTotalRequest(queryValue: String, page: Int) {
        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))
        urlComponent?.queryItems?.append(URLQueryItem(name: "page", value: page.stringValue))
        
        guard let url = urlComponent?.url else {
            return
        }
        
        var request = URLRequest(url: url)
        request.allHTTPHeaderFields = headers
        request.httpMethod = "GET"
        
        let session = URLSession(configuration: .default)
        session.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: BookModel.self, decoder: JSONDecoder())
            .map { model in
                if model.meta.isEnd == false {
                    return model.documents
                } else {
                    return []
                }
            }
            .replaceError(with: []) // 에러가 발생할땐 아무것도 없는 init 상태의 값 리턴.
            .assign(to: \.document, on: self)
            .store(in: &cancellables)
    }

그리고 page를 증가시켜도 현재 출력되는 값이 같다.

즉 page에 대한 query가 안먹는건지 의심이 든다.

페이지에 대한 쿼리는 작동하는걸로 확인했다.

그러면 willDisplay에서 호출한게 잘못된건지에 대한 생각도 드는데 그건 아닌것 같다 숫자는 제대로 들어가지만

1
2
3
4
5
6
7
8
9
func transform(input: Input, page: Int) {
        input.searchPublisher
            .print()
            .sink { [weak self] value in
            //self?.fetchRequest(queryValue: value)
                self?.fetchTotalRequest(queryValue: value, page: page)
                print(page)
        }.store(in: &cancellables)
    }

여기에 파라미터로 넣은 page가 문제이다. 즉 적용이 되지않는다는것,

1
2
3
4
struct Input { // searchBar input을 받기위함.
        let searchPublisher: AnyPublisher<String, Never>
        let numberPublisher: AnyPublisher<Int, Never> // added
    }

publisher를 추가하면 될것 같아서 하나 만들어주고 publisher들을 하나로 통합해준다.

1
2
3
4
5
6
7
8
9
10
11
12
func transform(input: Input) {
        
        Publishers.CombineLatest(input.searchPublisher, input.numberPublisher).flatMap { value, page in
            let tuple = (value, page)
            return Just(tuple)
        }
        .eraseToAnyPublisher()
        .sink { [weak self] data in
            self?.fetchTotalRequest(queryValue: data.0, page: data.1)
        }.store(in: &cancellables)
        
    }

그리고 vc로 가서

1
2
3
4
5
let numberSubject = PassthroughSubject<Int, Never>()
    
    var valuePublisher: AnyPublisher<Int, Never> {
        return numberSubject.eraseToAnyPublisher()
    }

하나 만들어주고,

viewdidload에 numberSubject.send(currentPage)플 하나 넣어준다.

input의 순서의 영향일까 저 트리거를 뒤에다가 해야 실행이 된다.

이제는 갱신이 된다.

지금은 vm에서 assign을 통해 계속 바꿔치기가 되고있다.

이제 이부분을 수정해야한다.

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
func fetchTotalRequest(queryValue: String, page: Int) {
        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))
        urlComponent?.queryItems?.append(URLQueryItem(name: "page", value: page.stringValue))
        
        guard let url = urlComponent?.url else {
            return
        }
        
        var request = URLRequest(url: url)
        request.allHTTPHeaderFields = headers
        request.httpMethod = "GET"
        
        let session = URLSession(configuration: .default)
        session.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: BookModel.self, decoder: JSONDecoder())
            .map { model in
                if model.meta.isEnd == false {
                    return model.documents
                } else {
                   return []
                }
            }
            .replaceError(with: [])
            .eraseToAnyPublisher()
            .sink(receiveValue: { model in
                self.document.append(contentsOf: model)
            })
            .store(in: &cancellables)
    }

아무리 생각을 해봐도 이것방법 말곤 떠오르지도 않고 찾아봐도 원하는게 잘 안보여서 이렇게 바꾼다.

May-09-2024 14-56-29

우선 완료.

타이머를 통한 스크롤시 과한 로딩 방지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if indexPath.section == 0 && indexPath.row == searchVM.document.count - 1 { // 마지막에 도달했을때
            Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(loadData), userInfo: nil, repeats: false)
           
        }
        
    }
    
    @objc func loadData() {
        searchVM.currentPage += 1
        searchVM.numberSubject.send(searchVM.currentPage)
        searchVM.transform(input: SearchVM.Input(searchPublisher: searchView.valuePublisher, numberPublisher: searchVM.valuePublisher))
        searchVM.$document
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
            .sink { [weak self] _ in
                self?.resultView.tableView.reloadData()
            }.store(in: &cancellables)
        print(searchVM.currentPage)
    }

타이머를 사용했다.

이렇게 0.5초마다 한번씩 실행하게 했다.

페이지가 과하게 올라가지 않음을 확인했다.

VM에서 alert 구현

현재 Coredata의 catch부분이 전부 비어있는데 이부분을 해결해보려 한다. 처음에 잘안되어서 잠시 보류했는데 어느정도 기능이 구현이 되고 튜터님께 여쭤보니 publisher를 통해 mainvc로 bool같은 데이터를 전달해서 mainvc가 그걸 받으면 처리해보는게 어떠냐고 하셨다.

이걸 다른 튜터님께도 여쭤봤는데 직접 사용 예시를 보여주셨기에 그부분을 구현해본다.

이글을 작성하는 시점에선 위의 방법을 쓰고 싶지만 머리가 돌아가지않아 이후에 다시 수정을 해보는걸로

1
2
3
enum Router {
        case alert(title: String, message: String, completion: (() -> Void)?)
    }

우선 alert에 대한 case하나 만들어주고, 안에는 UIAlertController 구성에 필요한 내용을 담아준다.

그리고 전달할 subject도 하나 만들어 준다.

1
var routerSubject = PassthroughSubject<Router, Never>()

그리고 do - catch 블럭에 다 넣어준다.

1
2
3
4
5
do {
    try context.save()
    } catch {
        routerSubject.send(Router.alert(title: "예외 발생", message: "\(error.localizedDescription) 이 발생했습니다."))
    }

그리고 vc에가서 호출

1
2
3
4
5
6
7
8
wishVM.routerSubject.sink { alert in
            switch alert {
            case .alert(let title, let message) :
                let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "확인", style: .default))
                self.present(alert, animated: true)
            }
        }.store(in: &cancellables)

simulator_screenshot_A68694E6-2735-4E10-89D9-FD7FF0C5B198

실제로 예외가 발생하고있다 어느순간 갑자기 발생하기 시작.

Wish 중복 걸러내기.

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
func getSpecificData (title: String) {
        let predicateRequest: NSFetchRequest<WishListModel> = WishListModel.fetchRequest()
        let predicate = NSPredicate(format: "title == %@", title)
        predicateRequest.predicate = predicate
        
        do {
            try context.fetch(predicateRequest).publisher.flatMap { data in
                Publishers.Sequence(sequence: [data])
            }
            .collect()
            .assign(to: \.wishDocument, on: self)
            .store(in: &cancellables)
        } catch {
            
        }
    }
    
func checkDuplicate (title: String) -> Bool {
        
        var flag = false
        getSpecificData(title: title)
        
        if wishDocument.isEmpty {
            flag = false
        } else {
            flag = true
        }
        return flag
    }

다음과 같이 중복확인을 위한 함수를 만들어 준다.

getSpecificData는 predicate를 통해 parameter로 받는 title과 Coredata에 있는 데이터의 title이 일치하는값을 가져온다.

그때 없으면 false, 있으면 true를 주어서 bool을 리턴한다.

처음에는 그냥 다시 다가져와서 filter를 통해서 카운트가 0이면 중복이 아니니 false를 리턴하게 했는데,

하나의 중복확인을 위해 데이터를 다 가져오는게 뭔가 별로라고 판단해서 아래의 코드는 폐기

1
2
3
4
5
6
7
8
9
10
11
12
func checkDuplicate (title: String) -> Bool {
        
        var flag = false
        getDocumentfromCoreData()
        
        if wishDocument.filter({ $0.title == title }).count == 0 {
            flag = false
        } else {
            flag = true
        }
        return flag
    }

이건 버튼 수정 내용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private lazy var getButton: UIButton = {
        let button = UIButton ()
        button.backgroundColor = .green
        button.setImage(UIImage(systemName: "bookmark.square"), for: .normal)
        button.tapPublisher.sink { [unowned self] _ in
            let vc = childViewController as? DetailViewController
            vc?.wishSubject.sink(receiveValue: { [unowned self] document in
                if vc?.wishVM.checkDuplicate(title: document.title) == false {
                    vc?.wishVM.saveDocumentToCoredata(data: document)
                    let alert = UIAlertController(title: "담기 완료", message: "책이 담겼습니다.", preferredStyle: .alert)
                    alert.addAction(UIAlertAction(title: "확인", style: .default, handler: { [unowned self] _ in
                        goToMainVC()
                    }))
                    vc?.present(alert, animated: true)
                } else {
                    let alert = UIAlertController(title: "중복 확인", message: "이미 리스트에 등록된 책입니다.", preferredStyle: .alert)
                    alert.addAction(UIAlertAction(title: "확인", style: .default))
                    vc?.present(alert, animated: true)
                }
                
            }).cancel()
        }.store(in: &cancellables)
        return button
    }()

May-09-2024 09-20-42

완료

최근 본 책에서 담기를 누르고 wish를 탭하면 계속 담기가 활성되는 문제 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lazy var getButton: UIButton = {
        let button = UIButton ()
        button.backgroundColor = .green
        button.setImage(UIImage(systemName: "bookmark.square"), for: .normal)
        button.tapPublisher.sink { [unowned self] _ in
            let vc = childViewController as? DetailViewController
            vc?.wishSubject.sink(receiveValue: { [unowned self] document in
                vc?.wishVM.saveDocumentToCoredata(data: document)
                let alert = UIAlertController(title: "담기 완료", message: "책이 담겼습니다.", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "확인", style: .destructive, handler: { [unowned self] _ in
                    goToMainVC()
                }))
                vc?.present(alert, animated: true)
            }).cancel() // modified
        }.store(in: &cancellables)
        return button
    }()

이상하게 컬렉션뷰에서 상세페이지를 띄우고 담은뒤, 위시버튼을 누르면 계속 똑같은게 담기길래

메모리에 해당 데이터가 계속 남는다고 판단하여 store(in: &cancellables) 대신 cancel()을 사용하여 subscription을 해제했다.

그리고 실행하여 테스트를하니 아무런 문제가 없음을 확인했다.

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