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])
}