10주차 과제 (6)
CollectionView에 적용.
이제 데이터 넘어오는것도 확인이 되었고 구현을 해보도록 하자.
우선 extension으로 관리할거니 파일을 하나 만들어주고.
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
extension MainViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionSetUp () {
recentView.collectionView.dataSource = self
recentView.collectionView.delegate = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
wishVM.wishDocument.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = recentView.collectionView.dequeueReusableCell(withReuseIdentifier: Constants.collectionViewCellIdentifier, for: indexPath) as? RecentCollectionViewCell else { return UICollectionViewCell() }
wishVM.$wishDocument
.receive(on: DispatchQueue.main)
.map { document -> RecentModel in
return document[indexPath.row]
}
.sink { model in
let image = URL(string: model.image!)
cell.imageView.kf.setImage(with: image)
cell.titleLabel.text = model.title
}.store(in: &cancellables)
return cell
}
}
우선은 이렇게 적어준다.
실행하니 보이지 않는다.
역시나 Cell에도 initializer가 없었다.
1
2
3
4
override init(frame: CGRect) { // added
super.init(frame: .zero)
layout()
}
실행
현재는 개판.
지금은 검색을해서 보고나면 바로 업데이트가 되지 않는다.
이부분은 추후 수정을 하도록 하고 먼저 컬렉션 뷰 부터 다시 손을 봐야겠다.
우선 layout을 수정하여 cell의 크기를 지정한다.
1
2
3
4
5
6
7
8
9
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 5 // added
layout.itemSize = .init(width: 220, height: 220) // added
var view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.register(RecentCollectionViewCell.self, forCellWithReuseIdentifier: Constants.collectionViewCellIdentifier)
return view
}()
그리고 컬렉션 뷰 역시 클릭하면 해당 정보를 가지고 올 수 있게 해주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let detailVC = DetailViewController()
wishVM.$wishDocument
.map { document in
return document[indexPath.row]
}.sink { 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
}.store(in: &cancellables)
present(detailVC, animated: true)
}
굿.
예외 해결
현재 검색하고 검색결과에 있는 리스트중 하나를 터치해서 들어가면 상세페이지가 나오는데,
누르고 다시 화면 복귀를 할때 업데이트가 되어야하는데 그렇지 않다.
vc의 생명주기를 고려한 메서드를 실행해도 되지 않았다.
즉 화면이 아예 가려지지 않아서, 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
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let detailVC = DetailViewController()
// DetailVC에 전달
searchVM.$document
.map{ document in
return document[indexPath.row]
}
.sink { document in
let imageURL = URL(string: document.thumbnail)
detailVC.titleView.titleLabel.text = document.title
detailVC.titleView.authorLabel.text = document.authors[0]
detailVC.imageView.imageView.kf.setImage(with: imageURL)
detailVC.imageView.priceLabel.text = document.price.stringValue
detailVC.bodyView.bodyLabel.text = document.contents
}.store(in: &cancellables)
// CoreData에 등록
searchVM.$document
.map{ document in
return document[indexPath.row]
}.sink(receiveValue: { [weak self] document in
self?.wishVM.saveDocumentToCoredata(data: document)
})
.store(in: &cancellables)
detailVC.modalPresentationStyle = .fullScreen // added
present(detailVC, animated: true)
}
이렇게 하면 이제 view가 완전히 사라지기에,
1
2
3
4
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
bind()
}
이걸 사용할수 있다.
이제는 처리가 잘된다.
collectionview에는 적용하지 않았다.
그리고 생각해보니 wishDocument가 아니라 recentDocument인데 명칭을 다르게 했다.
VM역시 RecentVM인데 WishVM으로 해서 이것도 수정했다.
정신이 없다.
실행화면
이제는 최근 본책에 바로 등록이 된다.
검색후 view 탭하면 키보드 내리기.
mainVC에서 tapPublisher를 하나 만들어준다.
1
2
3
4
5
6
7
private lazy var tapPublisher: AnyPublisher<Void, Never> = {
let tapGesture = UITapGestureRecognizer(target: self, action: nil)
view.addGestureRecognizer(tapGesture)
return tapGesture.tapPublisher.flatMap { _ in
Just(())
}.eraseToAnyPublisher()
}()
이 코드는 tip-calculator 공부하면서 알게되었다.
void이므로 Just에는 아무것도 없다.
그리고 observe 함수를 하나 만들어 준다.
1
2
3
4
5
private func observe() {
tapPublisher.sink { [unowned self] _ in
view.endEditing(true)
}.store(in: &cancellables)
}
이러면 이제 키보드가 내려간다.
모든 view에 해두니 다른게 먹지 않아서 해당기능은 폐기.
그냥 1초뒤에 키보드를 내리게 했다
1
2
3
4
5
6
7
8
9
10
// SearchView
private func observe() {
searchBar.searchTextField.textPublisher
.debounce(for: 1, scheduler: RunLoop.main) // 1초의 시간을 기다렸다가 전달.
.sink { [weak self] value in
self?.searchBarSubject.send(value)
self?.endEditing(true) // added
}.store(in: &cancellables)
}
WishList 구현.
이제 담기 기능을 구현해야한다.
디자인은 안했지만, 바로 이 초록색 버튼을 클릭했을때 담아져야한다.
지금 드는 생각은 send를 통해 현재 보고있는 model을 그대로 가져와서 담기할때 그걸 다시 내보내는 구조가 되어야 되지 않을까 라는 생각이 든다.
우선 detailVC에
var wishSubject = PassthroughSubject<Document, Never>()
subject를 하나 만들어준다.
그리고 tableview extension에서
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let detailVC = DetailViewController()
// DetailVC에 전달
searchVM.$document
.map{ document in
return document[indexPath.row]
}
.sink { document in
let imageURL = URL(string: document.thumbnail)
detailVC.titleView.titleLabel.text = document.title
detailVC.titleView.authorLabel.text = document.authors[0]
detailVC.imageView.imageView.kf.setImage(with: imageURL)
detailVC.imageView.priceLabel.text = document.price.stringValue
detailVC.bodyView.bodyLabel.text = document.contents
detailVC.wishSubject.send(document) // added
}.store(in: &cancellables)
이렇게 전달을 해주고,
detailVC로 돌아가서 viewDidLoad에
1
2
3
wishSubject.sink { value in
print(value)
}.store(in: &cancellables)
이걸 추가해서 제대로 전달이 되는지 확인을 해본다.
출력이 되지 않았다.
즉 값이 전달이 되지 않았다는 뜻이다.
지금은 일어나서 다시 코드를 확인하고 있는데, 저걸 작성했던 시점에서는 part5 글작성하고 바로 part6인 이 글을 작성하고 있어서 정신줄을 놨나보다.
아무래도 @published, assign
를 사용해서 우선 vm에 옮기고 그다음에 그걸 사용해서 하면 될것같다는 생각이 든다.
하지만 문제 발생
1
2
3
4
5
searchVM.$document
.map{ document in
return [document[indexPath.row]]
}.assign(to: \.wishDocument, on: wishVM)
.store(in: &cancellables)
여기와
1
2
3
4
5
6
7
8
9
10
11
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
wishVM.$wishDocument.sink { document in
print(document)
}.store(in: &cancellables)
}.store(in: &cancellables)
return button
}()
여기의 wishVM이 서로 다르다.
그게 무슨말이냐면
위에 있는 wishVM의 경우 mainVC에서 만들어진 인스턴스
아래에 있는 vm은 wishVC에서 만들어진 인스턴스
즉 둘의 이름은 같으나 엄연히 메모리도 다른 별개의 인스턴스이다.
그렇기에 아무리 호출을 해도 되지 않는다.
위에 send도 마찬가지.
튜터님께 여쭤보니 VM은 싱글턴을 잘 사용하지 않는다고한다.
어떻게 전달해야할지 막막해진다.
Publisher로 하는걸 폐기하고 한참을 고민하다가 subject중 PassthroughSubject
는 전달한 값을 들고있지않고,
CurrentValueSubject
는 전달한 값의 마지막값을 들고있는걸 생각했고, 바로 비교를 했다.
CurrentValueSubject는 initializing이 필요하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// cellForRowAt
detailVC.wishSub.send(document)
detailVC.wishSubject.send(document)
// detailVC
var wishSubject = CurrentValueSubject<Document, Never>(.init(authors: [], contents: "", price: 0, title: "", thumbnail: "", salePrice: nil))
var wishSub = PassthroughSubject<Document, Never>()
wishSubject
.print()
.sink { document in
print(document)
}.store(in: &cancellables)
wishSub
.print()
.sink { document in
print(document)
}.store(in: &cancellables)
결과
1
2
3
4
5
6
7
receive subscription: (CurrentValueSubject)
request unlimited
receive value: (Document(authors: ["조앤 K. 롤링"], contents: "선과 악의 대립 속에서 평범한 어린 소년이 한 사람의 영웅으로 성장해나가는 보편적인 테마를 바탕으로 빈틈없는 소설적 구성과 생생하게 살아 있는 캐릭터, 정교하게 만들어낸 환상의 세계를 접목시킨 21세기의 고전 『해리 포터와 마법사의 돌』 20주년 개정판. 해리 포터를 처음 만나는 어린 세대가 20년이 지나 성인의 눈높이에서 읽어도 어색함 없이 책을 통해 해리 포터 세계를 경험하며 기쁨을 만끽할 수 있도록 고전의 깊이로 담아냈다. 어둠의 마왕", price: 9000, title: "해리 포터와 마법사의 돌 1(해리포터 20주년 개정판)", thumbnail: "https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F5134210%3Ftimestamp%3D20240426133336", salePrice: nil))
Document(authors: ["조앤 K. 롤링"], contents: "선과 악의 대립 속에서 평범한 어린 소년이 한 사람의 영웅으로 성장해나가는 보편적인 테마를 바탕으로 빈틈없는 소설적 구성과 생생하게 살아 있는 캐릭터, 정교하게 만들어낸 환상의 세계를 접목시킨 21세기의 고전 『해리 포터와 마법사의 돌』 20주년 개정판. 해리 포터를 처음 만나는 어린 세대가 20년이 지나 성인의 눈높이에서 읽어도 어색함 없이 책을 통해 해리 포터 세계를 경험하며 기쁨을 만끽할 수 있도록 고전의 깊이로 담아냈다. 어둠의 마왕", price: 9000, title: "해리 포터와 마법사의 돌 1(해리포터 20주년 개정판)", thumbnail: "https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F5134210%3Ftimestamp%3D20240426133336", salePrice: nil)
receive subscription: (PassthroughSubject)
request unlimited
둘의 데이터 전달의 차이가 발생.
그래서 subject를 바꿔서 하기로 결정했다.
이제 이걸 buttonview의 클래스에 있는 버튼의 tappublisher를 통해서 위와 같은 내용이 출력이 되는지 확인을 해본다.
1
2
3
4
5
6
7
8
9
10
11
12
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: { document in
print(document)
}).store(in: &cancellables)
}.store(in: &cancellables)
return button
}()
다음과 같이 구현
테스트를 해본다. 출력이된다.
combine 개발자들은 이런상황까지 고려한걸까? 오늘도 깨달음을 하나 얻는다.
세부 로직 수정
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
//buttonview
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: { document in
vc?.wishVM.saveDocumentToCoredata(data: document)
}).store(in: &cancellables)
}.store(in: &cancellables)
return button
}()
//wishvm
func saveDocumentToCoredata (data: Document) {
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 {
try context.save()
print("담기 완료")
} catch {
}
}
이렇게 해주었다.
담기 버튼을 클릭하니 담기 완료가 뜬다.
db를 확인해보자.
확인완료.
coredata 가져오기.
wishVM에 다음과 같이 구현 recent와 상동
1
2
3
4
5
6
7
8
9
10
11
12
13
func getDocumentfromCoreData () {
do {
try context.fetch(request).publisher.flatMap { data in
Publishers.Sequence(sequence: [data])
}
.collect()
.assign(to: \.wishDocument, on: self)
.store(in: &cancellables)
} catch {
}
}
그리고 wishlistVC에는 다음과 같이 적는다
1
2
3
4
5
6
7
8
private func bind () {
wishVM.getDocumentfromCoreData()
wishVM.$wishDocument
.receive(on: DispatchQueue.main)
.sink { [unowned self] _ in
self.bodyTableView.tableView.reloadData()
}.store(in: &cancellables)
}
그리고 extension에서 wishlistVC의 tableView 메서드들 구현.
성공.
슬슬 AutoLayout깨진게 거슬리기 시작한다.