티스토리 뷰

글의 내용을 보충하는 의견, 잘못된 내용을 수정하는 의견 모두 다 환영합니다.

 

NSFetchedResultsControllerDelegate 프로토콜에 iOS 13부터 DiffableDatasource를 지원하기 위해 등장한 controller(_:didChangeContentWith:) 메소드가 있는데, ViewContext와 함께 이 메소드를 사용할 경우 놓칠 수 있는 주의점이 있어서 기록해둡니다.

문제 상황

한 개의 NSManagedObject를 생성하고 저장한 뒤, 이를 반복할 때 한 개의 셀만 애니메이팅 되어야하는데 두 개의 셀이 애니메이팅함.

동시에 두 개의 셀이 애니메이팅되고 있음

다음과 같은 상황을 가정

- 사용하는 ViewContext는 NSPersistentContainer에서 기본 제공하는 NSManagedObjectContext로, Main queue와 관련이 있고 NSPersistentCoordinator를 parent로 가지고 있음

- NSFetchedResultsController를 초기화시 ViewContext를 사용함

final class BreadListViewController {
	....
    
    private lazy var fetchedResultsController: NSFetchedResultsController<Bread> = {
        let fetchRequest = Bread.fetchRequest()
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "touch", ascending: false)]
        fetchRequest.fetchBatchSize = 50
        let controller = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: self.viewContext,
            sectionNameKeyPath: nil,
            cacheName: nil
        )
        controller.delegate = self
        
        return controller
    }()
    
    ...
}

- UITableView와 UIDiffableDataSource를 사용하여 테이블 뷰 구현

- NSFetchedResultsControllerDelegate > controller(_:didChangeContentWith:) 를 구현. 전달받은 snapshot을 그대로 dataSource에 apply하도록 구현

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
        diffableDataSource.apply(snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>)
    }

 

이때 새로운 FRC(NSFetchedResultsController)가 페칭하는 NSManagedObject를 ViewContext에서 생성하면 구현해둔 FRC Delegate 덕분에 테이블뷰를 전체 리로드하지 않더라도 적절한 애니메이션과 함께 셀이 추가됩니다. 이때, FRC Delegate에 넘겨온 Snapshot > itemIdentifiers 는 NSManagedObjectID 타입의 값들입니다.

 

새로운 NSMO(NSManagedObject)를 생성후 ViewContext.save()를 호출하지 않았다면, 새로 생성된 NSMO의 objectID는 temporaryID로 스냅샷에 넘어옵니다(아래 설명 참조).

https://developer.apple.com/documentation/coredata/nsmanagedobject/1506848-objectid

문제발생

문제는 여기서 발생합니다. 현재 DiffableDataSource에 temporaryID를 가진 snpashot이 적용되어 있을 때, temp ID를 가진 NSMO들을 저장한 다음, 다시 FRC가 페칭하는 NSMO를 생성하게 되면 저장된 NSMO의 permanentID가 snapshot에 넘어오면서 동시에 여러 개의 셀들의 삽입 애니메이션이 동작하게 됩니다(tempID -> permanentID로 바뀐 셀 + 새로 생성된 NSMO 셀). 과정을 정리해보겠습니다.

 

1. 첫 번째 NSMO 생성

2. viewContext.save() 호출

3. FRC Delegate snapshot 넘어오고, 첫 번째 NSMO의 temporaryID가 들어옴.

4. NSManagedObjectContextDidSave 노티 발급

5. 두 번째 NSMO 생성

6. viewContext.save() 호출

7. FRC Delegate 에 snapshot이 넘어오는데, 이때 두 번째 NSMO의 temp ID와 첫 번째 NSMO의 permanent ID가 넘어오면서 두 객체와 관련된 셀이 동시에 애니메이팅.

 

원래 의도한 건 'NSMO를 하나씩 추가하면 하나씩 삽입 애니메이션이 동작'인데,

temp ID > perament ID 로 바뀌면서 snapshot이 이것을 '다른' identifier 취급을 하여 생기는 현상입니다.

 

해결방법

1. ViewContext > automaticallyMergesChangesFromParent 값을 true로 켜준 뒤, NSMO를 생성할 때 다른 NSManagedObjectContext를 사용하여 NSPersistentCoordinator에 저장

=> FRC Delegate에 매번 permanent ID 만 넘어오도록 구현하는 방식

 

2. NSBatchInsertRequest를 사용하여 직접 영구저장소에 저장 후, 변경사항을 ViewContext에 반영

 

3. NSMO를 생성 후 NSManagedObjectContext.obtainPermanentIDs(for:) 메소드를 호출하여 permanent ID로 변환시킨다.

 

4. DiffableDataSource 안쓰기. 

 

1, 2, 3는 DiffableDataSource를 사용하는 방법이고 FRC Delegate가 permanent ID만 받도록 구현하여 문제를 해결한 방식입니다. 

4의 방법은 DiffableDataSource을 쓰지 않고 기존 UITableViewDataSource 프로토콜을 채택하여 테이블뷰를 구현하는 방식입니다. 그래서 본 문제도 발생하지 않습니다. 대신 iOS 13부터 등장한 CollectionDifference를 사용하는 FRC Delegate 메소드를 사용한다면, snapshot처럼 유려한 애니메이션을 누릴 수 있겠습니다. 

댓글