포스트

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

실행

Simulator Screenshot - iPhone 15 Pro - 2024-05-07 at 04 32 48

현재는 개판.

지금은 검색을해서 보고나면 바로 업데이트가 되지 않는다.

이부분은 추후 수정을 하도록 하고 먼저 컬렉션 뷰 부터 다시 손을 봐야겠다.

우선 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)
    }

굿.

May-07-2024 05-32-03

예외 해결

현재 검색하고 검색결과에 있는 리스트중 하나를 터치해서 들어가면 상세페이지가 나오는데,

누르고 다시 화면 복귀를 할때 업데이트가 되어야하는데 그렇지 않다.

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으로 해서 이것도 수정했다.

정신이 없다.

실행화면

May-07-2024 06-09-12

이제는 최근 본책에 바로 등록이 된다.

검색후 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)
    }

이러면 이제 키보드가 내려간다.

May-07-2024 07-03-47

모든 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 구현.

이제 담기 기능을 구현해야한다.

simulator_screenshot_2D511E82-F288-4D68-B546-8EF423592FD1

디자인은 안했지만, 바로 이 초록색 버튼을 클릭했을때 담아져야한다.

지금 드는 생각은 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를 확인해보자.

CleanShot 2024-05-07 at 18 10 07@2x

확인완료.

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 메서드들 구현.

simulator_screenshot_21564CD4-BAFC-47D6-B480-6130B6FBE080

성공.

슬슬 AutoLayout깨진게 거슬리기 시작한다.

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