포스트

10주차 과제 (5)

상세페이지 돌아가기 버튼 구현

1
2
3
4
5
6
7
8
9
private var closeButton: UIButton = {
        var button = UIButton ()
        button.backgroundColor = .gray
        button.setImage(UIImage(systemName: "x.circle"), for: .normal)
        button.tapPublisher.sink { [unowned self] _ in
            goToMainVC()
        }.store(in: &cancellables)
        return button
    }()

공부했던 내용을 바탕으로 이렇게 적었다.

tapPublisher를 통해 데이터를 전달하려는게 목적이 아닌,

goToMainVC 메서드를 호출하는데 목적이 있다.

CleanShot 2024-05-06 at 12 57 04@2x

error 발생.

lazy를 해줌으로써 해당버튼이 이후에 작동하게 순서를 바꿔준다.

그리고 goToVC 함수에는

1
2
3
private func goToMainVC() {
        childViewController?.dismiss(animated: true)
    }    

이렇게해서 현재 띄워져있는 VC에 대해 dismiss를 하라고 한다.

childViewController 찾지 못하는데

별도로 설정을 해줘야한다.

1
2
3
4
5
6
7
extension UIResponder {
    
    var childViewController: UIViewController? {
        return next as? UIViewController ?? next?.childViewController
    }
    
}

이건 childViewController에 computed property처럼 설정을 해두었는데, next를 사용하여 그게 VC인지를 확인하고 맞으면 그대로 사용하고, 그게아니면 체이닝을 통해 vc를 찾아서 리턴을 해준다.

next 속성은 응답 체인을 형성하는 데 사용된다.

응답 체인은 이벤트가 발생했을 때 이를 처리할 객체를 찾는 과정을 말한다. 이벤트가 발생하면 해당 객체에서 이벤트를 처리할 수 있는지 여부를 확인하고, 처리할 수 없는 경우 next 속성을 통해 다음 응답 객체로 이벤트를 전달한다. 계속 꼬리에 꼬리를 물어 찾는다.

UIResponder?

UIResponder??

UIApplication, UIViewController, UIView 객체 모두 Responder 그리고 처리되지 않은 앱의 다른부분을 전달하는 작업을 관리한다 ?

주어진 리스폰더가 일을 처리하지 않으면, 리스폰더 체인의 다음 이벤트로 해당 이벤트를 전달. 정의되어있는 규칙을 사용하여 동적으로 관리 ex) 하위 view에서 상위view로 전달하거나…

이런식으로 사슬로 연결되어있는거랄까…

좌측이 iOS, 우측이 macOS이다.

이미지 출처

https://blog.dunzo.com/swift-responder-chain-19a19fa0fadc

https://www.cocoanetics.com/2012/09/the-amazing-responder-chain/

담기 화면을 위한 Wishlist VC 세팅

이녀석은 탭바로 들어가기에

SceneDelegate에서 초기설정을 해준다.

1
2
3
4
5
6
7
8
let wishVC = WishlistViewController() // added

// added
wishVC.tabBarItem = UITabBarItem(
            title: "Wish",
            image: UIImage(systemName: "list.bullet.circle"),
            selectedImage: UIImage(systemName: "list.bullet.rectangle.fill"))
tabbarController.viewControllers = [firstVC,wishVC] // modified 

이렇게 해주면 구성끝

1
view.backgroundColor = .green

테스트를 위해 wishlist의 background를 green으로 변경.

simulator_screenshot_A70E1F0F-F87B-4B45-A914-152243FB35E9

색 선정도 참….

WishList VC 디자인

CleanShot 2024-05-06 at 15 12 10@2x

두개의 섹션으로 구분을 하면 될 듯 하다.

추가는 선택구현인데, 그냥 tabbar를 변경해주면 될것같다.

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
class WishlistViewController: UIViewController {
    
    private let headerView = HeaderView()
    private let bodyTableView = BodyTableView()
    
    private lazy var vStackView: UIStackView = {
        var stackView = UIStackView(arrangedSubviews: [
            headerView,
            bodyTableView,
            UIView()
        ])
        stackView.axis = .vertical
        stackView.spacing = 20
        return stackView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
      
        layout()
    }
    
    private func layout () {
        view.addSubview(vStackView)
        
        vStackView.snp.makeConstraints { make in
            make.top.equalTo(view.snp.top).offset(100)
            make.bottom.trailing.leading.equalToSuperview()
        }
        
        headerView.snp.makeConstraints { make in
            make.height.equalTo(100)
        }
        
        bodyTableView.snp.makeConstraints { make in
            make.height.equalTo(500)
        }
    }
    
}

초기 디자인

simulator_screenshot_97F2C10D-B70F-47F1-A305-3B087D0F8C2D

디자인 과정은 생략하겠다.

CoreData 세팅

CleanShot 2024-05-06 at 18 21 34@2x

우선 Coredata 모델링은 다음과 같다.

물론 WishListModel의 Attributes도 같게 해두었다.

AppDelegate에 설정을 해둔다.

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
// MARK: - Coredata
    lazy var persistentContainer: NSPersistentContainer = {
        
        let container = NSPersistentContainer(name: "CoreModel")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()
    
    // MARK: - Core Data Saving support
    
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }

어디에 적어두면 CoreData쓸때 항상 쓰는 기능.

셀 클릭시 Coredata에 저장

이부분은 시도를 해본적이 없어서 과정을 좀 적어본다.

검색을하고 클릭을하면 최근에 본 책에 필요한 Coredata로 값이 전달이 되어야한다.

될 것 같다.

우선 tableview의 cellforrowat 함수도 수정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let detailVC = DetailViewController()
        
        searchVM.$document
        .receive(on: DispatchQueue.main)
        .sink { document in // added
            let imageURL = URL(string: document[indexPath.row].thumbnail)
            detailVC.titleView.titleLabel.text = document[indexPath.row].title
            detailVC.titleView.authorLabel.text = document[indexPath.row].authors[0]
            detailVC.imageView.imageView.kf.setImage(with: imageURL)
            detailVC.imageView.priceLabel.text = document[indexPath.row].price.stringValue
            detailVC.bodyView.bodyLabel.text = document[indexPath.row].contents
        }.store(in: &cancellables)
        
        present(detailVC, animated: true)
    }

기존에 있던걸 이렇게 바꾸었다 이때 assign을 통해 RecentVM에 전달이 가능할것 같아서 해본다.

한번보내면 끝나는 combine 특성상 하나를 더 만들어서 RecentVM의 document로 보낸다

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
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let detailVC = DetailViewController()
        
        searchVM.$document
            .receive(on: DispatchQueue.main)
            .map{ document -> 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)
        
        // added
        searchVM.$document
            .map{  document -> Document in
            return document[indexPath.row]
            }.assign(to: \.wishDocument, on: recentVM.self)
            .store(in: &cancellables)
        
        present(detailVC, animated: true)
    }

프린트를 통해 출력을 해보니

1
2
3
receive subscription: (PublishedSubject)
request unlimited
receive value: (Document(authors: ["조앤 K. 롤링"], contents: "영화 〈해리 포터〉와 〈신비한 동물사전〉 시리즈에서 비주얼 그래픽을 담당했던 스튜디오 ‘미나리마(MinaLima)’가 직접 디자인한 시리즈 ‘미나리마 에디션’이 드디어 세 번째 이야기 《해리 포터와 아즈카반의 죄수》를 선보인다. 《해리 포터와 아즈카반의 죄수: 미나리마 에디션》에서는 영화와는 다른 모습의 시리우스와 루핀, 크룩섕스와 벅빅을 만날 수 있으며, 미나리마가 오직 이 책만을 위해 만들어 낸 8가지 공작 요소를 통해 해리 포터의 마법 세계", price: 38000, title: "해리 포터와 아즈카반의 죄수(미나리마 에디션)", thumbnail: "https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F6455866%3Ftimestamp%3D20240419171325", salePrice: nil))

잘 넘어가는 듯 하다.

그다음 vm에서 coredata에관한 코드를 작성하다 문득 든 생각인데, 굳이 이걸 다시 assign으로 담아서 보내야 하나? 라는 생각이 들었다.

그냥 클릭했을때 해당 document에 대해서 바로 coredata에 넘기면 되는것 아닌가? 라는 생각이 든다.

RecentVM 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class RecentVM {
    
    let context = (UIApplication.shared.delegate as! AppDelegate) .persistentContainer.viewContext
    
    func saveDocumentToCoredata (data: Document) {
        
        let newItem = RecentModel(context: context)
        newItem.title = data.title
        newItem.author = data.authors[0]
        newItem.content = data.contents
        newItem.image = data.thumbnail
        newItem.price = Int64(data.price)
        newItem.date = Date().timeIntervalSince1970
        
        do {
            try context.save()
        } catch {

        }
            
    }
}

여기는 coredata에 저장할 목적으로 다음과 같이 사용했다.

date는 흔히 많이 사용하는 Date().timeIntervalSince1970를 사용했다. 이렇게 되면 Double형식으로 되는데, 이것의 숫자 값을 비교하여 최근 본순서대로 정리를 하면된다.

그리고 didSelectRowAt 역시 수정을 해주었다.

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
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let detailVC = DetailViewController()
        
        searchVM.$document
            .receive(on: DispatchQueue.main)
            .map{ document -> 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)
        
        // modified
        searchVM.$document
            .map{  document -> Document in
            return document[indexPath.row]
            }.sink(receiveValue: { [weak self] document in
                self?.recentVM.saveDocumentToCoredata(data: document)
            })
            .store(in: &cancellables)
        
        present(detailVC, animated: true)
    }

실행을해서 CoreData에 들어오는지 확인을 해주면 될것같다.

우선 DB위치 확인을 위해 print(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask))를 적어서 호출

CleanShot 2024-05-07 at 00 28 59@2x

확인완료.

이젠 CoreData를 request로 호출하여 CollectionView에 띄우면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
    @Published var wishDocument = [RecentModel]()
    private var cancellables = Set<AnyCancellable>()
    
    func getDocumentfromCoreData () {
        
        do {
            try context.fetch(request).publisher
                .assign(to: \.wishDocument, on: self)
                .store(in: &cancellables)
        } catch {
            
        }
    }

마찬가지로 예외에 대한 catch는 뒤에 하기로하고. 처음에는 이렇게 했더니 type error가 발생한다.

wishDocument는 [RecentModel]의 배열인데, 리턴을 하려는건 RecentModel인 집합이 아니었기때문.

그래서 operator를 사용하여 배열을 안에 넣고 리턴하게 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
func getDocumentfromCoreData () {
        
        do {
            try context.fetch(request).publisher
                .map { data in // added
                    return [data]
                }
                .assign(to: \.wishDocument, on: self)
                .store(in: &cancellables)
        } catch {
            
        }
    }

VC에서 호출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private func bind () {
        searchVM.transform(input: SearchVM.Input(searchPublisher: searchView.valuePublisher))
        searchVM.$document
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.resultView.tableView.reloadData()
            }.store(in: &cancellables)
        
        recentVM.$wishDocument
            .print()
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.recentView.collectionView.reloadData()
            }.store(in: &cancellables)
    }

이제 두개의 내용에 대해 전부 포함하는 bind 함수를 구현한다

wishDocument에 print를 해둔건. 제대로 출력이 되는지에 대해 확인이 필요해서 해두었다.

우선 실행해보니 coredata에 이미 테스트용 으로 해둔게 있는데 로드가 되지않은 문제를 확인했다.

1
2
3
receive subscription: (PublishedSubject)
request unlimited
receive value: ([])

생각해보니 애초에 해당 메서드를 호출하지 않았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private func bind () {
        searchVM.transform(input: SearchVM.Input(searchPublisher: searchView.valuePublisher))
        searchVM.$document
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.resultView.tableView.reloadData()
            }.store(in: &cancellables)
        
        recentVM.getDocumentfromCoreData() // added
        recentVM.$wishDocument
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.recentView.collectionView.reloadData()
            }.store(in: &cancellables)
    }

출력을 확인해보니

1
2
3
receive subscription: (PublishedSubject)
request unlimited
receive value: ([<Book.RecentModel: 0x60000213f1b0> (entity: RecentModel; id: 0x955587754e7d6e80 <x-coredata://B14523BD-6BBF-4798-8817-D4116D7B1FEF/RecentModel/p3>; data: <fault>)])

이렇게 확인이 된다.

제대로 데이터가 들어왔는지 확인을 위해 변수를 하나 만들어서 print를 해봐야겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 private func bind () {
        searchVM.transform(input: SearchVM.Input(searchPublisher: searchView.valuePublisher))
        searchVM.$document
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.resultView.tableView.reloadData()
            }.store(in: &cancellables)
        
        recentVM.getDocumentfromCoreData()
        recentVM.$wishDocument
            .receive(on: DispatchQueue.main)
            .sink { [weak self] data in
                self?.testArray = data // 확인작업
                self?.recentView.collectionView.reloadData()
                print(self?.testArray[0].title) // 확인작업
            }.store(in: &cancellables)
    }

출력결과.

1
Optional("해리 포터와 마법사의 돌 1(해리포터 20주년 개정판)")

데이터 들어오는게 확인이 되었다.

데이터 소팅.

들어오는것이 확인이 되었으니, 우리는 소팅을 해줘야한다.

애초에 assign을 통해 wishDocument에 넣을때 소팅한값이 들어모면 되지않을까? 라는 생각에 VM 쪽 코드를 다듬어본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func getDocumentfromCoreData () {
        
        do {
            try context.fetch(request).publisher
                .map { data in
                    return [data].sorted { first, second in // modified
                        first.date > second.date
                    }
                }
                .assign(to: \.wishDocument, on: self)
                .store(in: &cancellables)
        } catch {
            
        }
    }

우선 이렇게 바꾸었다.

역시나 확인을 위해 vc에서 테스트를 해본다.

문제 발견.

테스트를 하던 도중 document가 하나만 넘어오는듯하다.

vm에서 print를 해보니 하나씩 리턴을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func getDocumentfromCoreData () {
        
        do {
            try context.fetch(request).publisher
                .map { data in
                    var array = [RecentModel]()
                    array.append(data)
                    print(array.count)
                    return array
                }
                .assign(to: \.wishDocument, on: self)
                .store(in: &cancellables)
        } catch {
            
        }
    }

이렇게 해서 프린트 한결과가

1
2
3
1
1
1

이었다.

아까전에 배열이아닌 모델로 타입 미스에대한 내용이 이것이었다.

나의 지식선에선 해결이 불가능해서 결국 gpt를 쓴다.

1
2
3
.flatMap { data in
        Publishers.Sequence(sequence: [data])}
.collect()

flatMap과 collect 그리고 sequence를 사용을 했다.

1
2
3
4
5
6
7
8
9
10
do {
            try context.fetch(request).publisher
                .flatMap { data in
                    Publishers.Sequence(sequence: [data])}
                .collect()
                .assign(to: \.wishDocument, on: self)
                .store(in: &cancellables)
        } catch {
            
        }

저건 [data]를 publisher로 만들게 된다.

즉 [data] publisher가 coredata에 있는 개수만큼 생성.

이후 flatmap을 통해 1개의 publisher로 통합. 그리고 collect를 사용하여 배열로 리턴.

CleanShot 2024-05-07 at 03 57 53@2x

사진은 collect

CleanShot 2024-05-07 at 04 00 42@2x

이건 sequence

map을 사용하면서 결과값은 항상 단일 모델이 될 수 밖에 없었다.

난 계속 되는줄 알고 배열로 씌운거였는데, 애초에 xcode는 그걸 알고서 타입이 맞지않는다고 했던것이다.

GPT 사용에 있어서 부정적인데, 이번에는 도움을 받긴했지만, 뭔가 검색을 하면서 한게 아니라서 그런가 썩 좋지는 않다.

다시 돌아와서 collect 다음에 map을 한번더 사용하여 소팅을 해준다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func getDocumentfromCoreData () {
        
        do {
            try context.fetch(request).publisher
                .flatMap { data in
                    Publishers.Sequence(sequence: [data])}
                .collect()
                .map { data in // added
                    var sorted = data.sorted { first, second in
                        first.date > second.date
                    }
                    return sorted
                }
                .assign(to: \.wishDocument, on: self)
                .store(in: &cancellables)
        } catch {
            
        }
    }

이렇게되면 이제 최근에 본 순서대로 배열이 정리가 된다.

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