포스트

10주차 과제 (Fin)

didSelectPublisher 사용.

지금도 충분히 끝나긴 했는데, tableview의 didSelectRowAt 메서드 대신

이걸 이용해보려고 한다.

1
2
3
tableView.didSelectRowPublisher.sink { indexPath in
            print(indexPath.row)
        }.store(in: &cancellables)

코드를 이렇게 작성한다.

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
39
40
41
42
43
44
45
46
47
48
49
lazy var tableView: UITableView = {
        let tableView = UITableView()
        tableView.backgroundColor = .systemBackground
        tableView.register(ResultTableViewCell.self, forCellReuseIdentifier: Constants.tableViewCellIdentifier)
        //tableView.allowsSelection = true // 셀을 선택할수있게 한다.
        tableView.didSelectRowPublisher.sink { [weak self] indexPath in
            if let mainVC = self?.childViewController as? MainViewController {
                let detailVC = DetailViewController()
                
                // DetailVC에 전달
                mainVC.searchVM.$document
                    .map{ document in
                        return document[indexPath.row]
                    }
                    .eraseToAnyPublisher()
                    .receive(on: DispatchQueue.main)
                    .sink { [weak detailVC] document in
                        let imageURL = URL(string: document.thumbnail)
                        detailVC?.titleView.titleLabel.text = document.title
                        detailVC?.titleView.authorLabel.text = document.authors.joined()
                        detailVC?.imageView.imageView.kf.setImage(with: imageURL)
                        detailVC?.imageView.priceLabel.text = document.price.stringValue
                        detailVC?.bodyView.bodyLabel.text = document.contents
                        detailVC?.wishSubject.send(document)
                    }.store(in: &detailVC.cancellables)
                
                // CoreData에 등록
                mainVC.searchVM.$document
                    .map{  document in
                        if !document.isEmpty {
                            return document[indexPath.row]
                        } else {
                            return document[0]
                        }
                    }
                    .eraseToAnyPublisher()
                    .sink(receiveValue: { [unowned self] document in
                        mainVC.recentVM.saveDataToRecent(data: document)
                    }).cancel()
                
                detailVC.modalPresentationStyle = .fullScreen
                mainVC.present(detailVC, animated: true)
                
            }
            
        }.store(in: &cancellables)
        tableView.rowHeight = 80
        return tableView
    }()

단지 차이점이라면, mainVC를 uiview에서 찾아서 해야한다는것.

이건 이전에 UIresponder Extension을 통해 구현을 해두었기에 사용이 가능.

다만 이것 역시 delegate를 비활성해야 가능하다.

collection view도 위와 같이 고쳐준다.

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
lazy var collectionView: UICollectionView = {
        
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.minimumLineSpacing = 5
        layout.itemSize = .init(width: 220, height: 220)
        
        var view = UICollectionView(frame: .zero, collectionViewLayout: layout)
        view.register(RecentCollectionViewCell.self, forCellWithReuseIdentifier: Constants.collectionViewCellIdentifier)
        
        view.didSelectItemPublisher
            .sink { [weak self] indexPath in
                if let mainVC = self?.childViewController as? MainViewController {
                    let detailVC = DetailViewController()
                    
                    mainVC.recentVM.$recentDocument
                        .map { document in
                            return document[indexPath.row]
                        }
                        .eraseToAnyPublisher()
                        .receive(on: DispatchQueue.main)
                        .sink { [weak detailVC] model in
                            let imageURL = URL(string: model.image ?? "")
                            detailVC?.titleView.titleLabel.text = model.title
                            detailVC?.titleView.authorLabel.text = model.author
                            detailVC?.imageView.imageView.kf.setImage(with: imageURL)
                            detailVC?.imageView.priceLabel.text = model.price.stringValue
                            detailVC?.bodyView.bodyLabel.text = model.content
                            detailVC?.wishSubject.send((mainVC.recentVM.convertModel(input: model)))
                        }.store(in: &mainVC.cancellables)
                    
                    mainVC.present(detailVC, animated: true)
                }
            }.store(in: &cancellables)
        
        return view
    }()

완료.

willdisplay 변경

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
lazy var tableView: UITableView = {
        let tableView = UITableView()
        tableView.backgroundColor = .systemBackground
        tableView.register(ResultTableViewCell.self, forCellReuseIdentifier: Constants.tableViewCellIdentifier)
        tableView.rowHeight = 80
        tableView.didSelectRowPublisher
            .sink { [weak self] indexPath in
                if let mainVC = self?.childViewController as? MainViewController {
                    let detailVC = DetailViewController()
                    
                    // DetailVC에 전달
                    mainVC.searchVM.$document
                        .map{ document in
                            return document[indexPath.row]
                        }
                        .eraseToAnyPublisher()
                        .receive(on: DispatchQueue.main)
                        .sink { [weak detailVC] document in
                            let imageURL = URL(string: document.thumbnail)
                            detailVC?.titleView.titleLabel.text = document.title
                            detailVC?.titleView.authorLabel.text = document.authors.joined()
                            detailVC?.imageView.imageView.kf.setImage(with: imageURL)
                            detailVC?.imageView.priceLabel.text = document.price.stringValue
                            detailVC?.bodyView.bodyLabel.text = document.contents
                            detailVC?.wishSubject.send(document)
                        }.store(in: &detailVC.cancellables)
                    
                    // CoreData에 등록
                    mainVC.searchVM.$document
                        .map{  document in
                            if !document.isEmpty {
                                return document[indexPath.row]
                            } else {
                                return document[0]
                            }
                        }
                        .eraseToAnyPublisher()
                        .sink(receiveValue: { [unowned self] document in
                            mainVC.recentVM.saveDataToRecent(data: document)
                        }).cancel()
                    
                    detailVC.modalPresentationStyle = .fullScreen
                    mainVC.present(detailVC, animated: true)
                }
            }.store(in: &cancellables)
        tableView.willDisplayCellPublisher.sink { [weak self] cell, indexPath in
            if let mainVC = self?.childViewController as? MainViewController {
                if indexPath.section == 0 && indexPath.row == mainVC.searchVM.document.count - 1 { // 마지막에 도달했을때
                    Timer.scheduledTimer(timeInterval: 0.5, target: self!, selector: #selector(self?.loadData), userInfo: nil, repeats: false)
                }
            }
        }.store(in: &cancellables)
        return tableView
    }()
    
    @objc func loadData() {
        if let mainVC = childViewController as? MainViewController {
            mainVC.searchVM.currentPage += 1
            mainVC.searchVM.numberSubject.send(mainVC.searchVM.currentPage)
            mainVC.searchVM.$document
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
                .sink { _ in
                    mainVC.resultView.tableView.reloadData()
                }.store(in: &cancellables)
        }
    }

전부 옮겨 준다.

근데 생각보다 길어지는데 이게 맞나 싶다.

작동은 된다.

swipeaction이 가능한지는 좀 더 확인해봐야할듯.

해당 코드가 너무 길어져서 튜터님께 여쭤보니

해당부분을 VM에서 해보는것도 좋다고 하신다.

VC의 bind 함수부분 변경

[2024.05.15 수정]

1
2
3
4
5
6
7
8
9
10
 CoredataManager.shared.routerSubject
            .receive(on: DispatchQueue.main)
            .sink { router in
            switch router {
            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)

이부분이 observe와 다른 개념으로 존재.

그 이유는 CoredataManager.shared.routerSubject 저 부분은 변화를 관측하고 alert를 띄우는게 아니기때문이다.

현재 모든 기능의 예외처리가

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

이렇게 되어있다.

우선 열거형인 Router를 파일로 만들어준다.

1
2
3
enum Router {
    case alert(title: String, message: String)
}

그리고 catch 부분을 다음과 같이 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func getWishDocumentfromCoreData () -> Future<[WishListModel] ,Error>{
        
        return Future<[WishListModel], Error> { [unowned self] complete in
            do {
                try context.fetch(wishRequest).publisher.flatMap { data in
                    Publishers.Sequence(sequence: [data])
                }
                .collect()
                .eraseToAnyPublisher()
                .sink(receiveValue: { model in
                    complete(.success(model))
                })
                .store(in: &cancellables)
            } catch {
                complete(.failure(error)) // modified
            }
        }
    }

그리고 vm에서는 다음과 같이 수정을 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
func getDocument () {
        coredataManager.getRecentDocumentfromCoreData().sink { complete in
            switch complete {
            case .finished:
                return
            case .failure(let error):
                self.routerSubject.send(Router.alert(title: "예외 발생", message: "\(error.localizedDescription) 이 발생했습니다.")) // added
            }
        } receiveValue: {[weak self] model in
            self?.recentDocument = model
        }
        .store(in: &cancellables)
    }

이젠 실패하게되면 routerSubject로 전달을 하고,

VC에선 이걸 받기만 하면된다.

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
private func bind () {
        
        searchVM.transform(input: SearchVM.Input(searchPublisher: searchView.valuePublisher, numberPublisher: searchVM.valuePublisher))
        searchVM.numberSubject.send(1)
        searchVM.$document
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.configureSnapshot()
            }
            .store(in: &cancellables)
        
        recentVM.getDocument()
        recentVM.$recentDocument
            .receive(on: DispatchQueue.main)
            .sink { [weak self] data in
                self?.collectionConfigureSnapshot()
            }.store(in: &cancellables)
        
        
        recentVM.routerSubject.sink { router in // modified
            switch router {
            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)
        
    }

wishVM도 동일하게 수정.

그리고 return이 없는 기능들은

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func saveWishDocumentToCoredata (data: Document, completion: @escaping ((Result<Void, Error>) -> Void)) {
        
        let newItem = WishListModel(context: context)
        newItem.title = data.title
        newItem.author = data.authors[0]
        newItem.content = data.contents
        newItem.image = data.thumbnail
        newItem.price = Int64(data.price)
        
        do {
            completion(.success(
                try context.save()
            ))
        } catch {
            completion(.failure(error))
        }
        
    }

다음과 같이 escaping closure를 사용했다.

그리고 vm에서도 이렇게 바꿔주었다.

1
2
3
4
5
6
7
8
9
10
11
func saveDatatoWish (data: Document) {
        coredataManager.saveWishDocumentToCoredata(data: data) { result in
            switch result {
            case .success(_):
                print("등록 완료")
                return
            case .failure(let error):
                self.routerSubject.send(Router.alert(title: "예외 발생", message: "\(error.localizedDescription) 이 발생했습니다."))
            }
        }
    }

테스트를 해본다.

만약 success로 간다면 등록완료가 콘솔에 출력이될것.

콘솔에 출력이 되는걸 확인했다.

나머지도 바꿔준다.

1
2
3
4
5
6
7
8
9
10
func deleteSpeificData (selectedCell: NSManagedObject, completion: @escaping ((Result<Void, Error>) -> Void)) {
        do {
            context.delete(selectedCell)
            try context.save()
            completion(.success(()))
        } catch {
            completion(.failure(error))
        }
        
    }

여기서 success에는 void이므로 context.delete(selectedCell) 이게 success안에 들어갈 수가 없다.

그래서 어차피 실행되면 저장을 하고 success에도 그냥 빈걸 리턴시켜버린다.

vm에서도 그냥 이렇게 처리

1
2
3
4
5
6
7
8
9
10
func deleteSelectedData(selectedCell: NSManagedObject) {
        coredataManager.deleteSpeificData(selectedCell: selectedCell) { result in
            switch result {
            case .success(_): // 이렇게 그냥 리턴
                return
            case .failure(let error):
                self.routerSubject.send(Router.alert(title: "예외 발생", message: "\(error.localizedDescription) 이 발생했습니다."))
            }
        }
    }

완료.

의존성 주입

[2024.05.14 수정]

SearchVM 쪽에 싱글턴으로 구현했는데 의존성 주입을 하면 좋다고 하셔서 고쳐본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
NetworkManager.shared.fetchTotalRequest(queryValue: value, page: page)
                    .sink { completion in
                        switch completion {
                        case .finished:
                            return
                        case .failure(_):
                            return
                        }
                    } receiveValue: { [weak self] documents in
                        documents.forEach { doc in
                            self?.document.append(doc)
                        }
                    }.store(in: &self!.cancellables)

위의 코드는 싱글턴 패턴을 적용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let networkManager = NetworkManager()

self?.networkManager.fetchTotalRequest(queryValue: value, page: page) // modified
                    .sink { completion in
                        switch completion {
                        case .finished:
                            return
                        case .failure(_):
                            return
                        }
                    } receiveValue: { [weak self] documents in
                        documents.forEach { doc in
                            self?.document.append(doc)
                        }
                    }.store(in: &self!.cancellables)

수정완료.

AlertManager 구현

[2024.05.14 수정]

1
2
3
4
5
6
7
8
9
class AlertManager {
    
    func makeAlert (title: String, message: String, style: UIAlertController.Style = .alert, completionHandler: @escaping ((UIAlertAction) -> Void)) -> UIAlertController {
        let alert = UIAlertController(title: title, message: message, preferredStyle: style)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: completionHandler))
        
        return alert
    }
}

다음과 같이 구현한다.

그리고 vc도 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        
        let deleteButton = UIContextualAction(style: .normal, title: "삭제") { (UIContextualAction, UIView, success: @escaping (Bool) -> Void)  in
            
            let alert = self.alertManager.makeAlert(title: "삭제하기", message: "정말 삭제하실 건가요?") { [unowned self] _ in
                wishVM.deleteSelectedData(selectedCell: wishVM.wishDocument[indexPath.row])
                wishVM.wishDocument.remove(at: indexPath.row)
                
            }
            success(true)
            self.present(alert,animated: false)            
        }
        
        deleteButton.backgroundColor = .red
        return UISwipeActionsConfiguration(actions: [deleteButton])
    }
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.