안녕하세요! 후르륵짭짭 입니다.
이번에는 CoreData에서 정말 유용하면서도 최고의 데이터 관리 API인
NSFetchResultController에 대해 알아보도록 하겠습니다.
** NSFetchResultController란 **
NSFetchedResultsController란 CoreData fetch 요청의 결과를 관리하거나 사용자에게 데이터를 보여주기 위해 사용하는 Controller라고 정의 되어 있습니다.
이게 무슨 말인가 하면,,,
//CoreData로 부터 데이터 가져오기
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func getAllFriends() -> [Friend]?{
do{
let items = try context.fetch(Friend.fetchRequest())
if let friends = items as? [Friend] {
return friends
}
}
catch let err{
print(err.localizedDescription)
}
return nil
}
func getAllMessages() -> [Message]?{
guard let friends = getAllFriends() else {return nil}
var messages : [Message] = []
friends.forEach { (friend) in
do{
let fetchRequest : NSFetchRequest<Message> = Message.fetchRequest()
// 시간이 작은 것(오래된 것)이 먼저 오게 된다
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
fetchRequest.predicate = NSPredicate(format: "chat_friend.name = %@", friend.name!)
fetchRequest.fetchLimit = 1
let message = try context.fetch(fetchRequest)
messages.append(contentsOf: message)
}
catch let err {
print(err.localizedDescription)
}
}
messages = messages.sorted(by: { (mess1, mess2) -> Bool in
return mess1.date! > mess2.date!
})
return messages.isEmpty ? nil : messages
}
이렇게 각 Friend 객체에 최근에 온 Message를 가져오고 날짜 순으로 정렬 한 데이터를 반환하기 위해서는 이렇게 복잡한 작업을 해줘야 했습니다.
그리고 아래 처럼 [Message] 배열을 가져오게 됩니다....
//ViewDidLoad에서 CoreData 가져오기
var messages : [Message]?
let dataControl = coreDataControl.shared
override func viewDidLoad() {
super.viewDidLoad()
dataControl.saveSomeMessage()
messages = dataControl.getAllMessages()
}
정말 복잡한 작업을 거쳐야하는데,,,
NSFetchResultsController를 사용하면 코드량을 줄이고 간단하고 쉽게 작업 할 수 있습니다.
** NSFetchResultsController 적용하기 **
lazy var fetchResultController : NSFetchedResultsController<Friend> = {
let fetchRequest : NSFetchRequest<Friend> = Friend.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastMessgae.date", ascending: false)]
let fetchResult = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: dataControl.context, sectionNameKeyPath: nil, cacheName: nil)
fetchResult.delegate = self
return fetchResult
}()
일단 Closure를 이용해서 fetchResultController를 정의 해줍니다.
하나씩 알아 볼 것인데, 결국 위에와 같은 모양을 가지게 됩니다.
그럼 알아 볼 것인데, 반드시 필요한 것이 있습니다.
1. 반환 되는 Generic 타입을 정의해줘야한다.
2. sortDesriptors를 정의해줘야합니다.
3. 결과적으로 FetchResultsController를 반환해줘야합니다.
이 세가지만 지켜주면 우리가 원하는 반복적인 작업의 FetchResultsController를 생성 할 수 있습니다.
let fetchResultController : NSFetchedResultsController<반환_타입> = {
(1)
let fetchRequest : NSFetchRequest<반환_타입> = 반환_타입.fetchRequest()
(2)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: 정렬_키 , ascending: 내림차순, 오름차순)]
(3)
let fetchResult = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: ViewContext를 반환 , sectionNameKeyPath: 섹션분류기준, cacheName: nil)
return fetchResult
}()
이렇게 반드시 필요한 세가지를 정의해주시면 fetchReusltController를 정의 한 것 입니다.
그리고 이제 ViewDidLoad()에 돌아가서 다음 코드를 추가 해줍니다.
//ViewDidLoad()에 다음 코드 추가
do{
try fetchResultController.performFetch()
print("fetch finish")
}
catch let err{
print("Freind Fetch Error" , err.localizedDescription)
}
이렇게 수행하면 fetchResultController에서 CoreData가 들어오게 됩니다.
그런데!! 여기서 중요한 것은!! CoreData에서 일단 데이터를 저장하고 난 후에 사용해주셔야합니다.
일반적인 경우에는 상관 없지만, 내부적으로 저장해야할 요소가 있다면, 아래 처럼 먼저 Data를 저장하고 나서
performFetch()를 수행 해줘야 합니다.
override func viewDidLoad() {
super.viewDidLoad()
//데이터를 performFetch() 이전에 가져와야 한다.
dataControl.saveSomeMessage()
do{
try fetchResultController.performFetch()
print("fetch finish")
}catch let err{
print("Freind Fetch Error" , err.localizedDescription)
}
,,,
}
위에 처럼 말 입니다!
이렇게 하면 이제 CoreData에 대한 모든 정보들이 FetchResultsController에 저장이 됐습니다.
그리고 이제 본격적으로 사용을 해보도록 하겠습니다.
** 본격적인 fetchResultController 사용 **
- Fetch 된 모든 데이터 가져오는 방법 -
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages?.count ?? 0
}
CollectionView나 TableView의 구성할 때 위에 처럼 총 count의 갯수로 호출 해줬습니다.
그런데 fetchResultsController를 사용하면 아래 처럼 사용 할 수 있습니다.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return fetchResultController.sections?[0].numberOfObjects ?? 0
}
우리는 section의 갯수가 0개 이기 때문에 위에 처럼 해주고 numberofObjeects 해주면 모든 Array의 갯수를 반환해줍니다.
- 특정 Index의 데이터를 가져오는 방법 -
//데이터를 선택 했을 때 특정 데이터 가져오는 방법
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let nextVC = ChatViewController()
nextVC.friend = messages![indexPath.item].chat_friend
//너의 일을 내가 대신 해줄게
nextVC.delegate = self
navigationController?.pushViewController(nextVC, animated: true)
}
CollectionView의 특정 Cell을 선택 했을 때, 데이터를 가져오는 방법을 보통 위에 처럼 IndexPath.item으로 가져왔습니다.
하지만 fetchResultsController를 사용하면 좀 더 다른 방식으로 가져 올 수 있습니다.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let test = fetchResultController.object(at: indexPath)
let nextVC = ChatViewController()
nextVC.friend = test
//너의 일을 내가 대신 해줄게
nextVC.delegate = self
navigationController?.pushViewController(nextVC, animated: true)
}
message 배열을 사용하지 않고 fetchResultController.object(at : IndexPath)를 사용해서 가져 올 수 있습니다.
사실 이렇게 정보만 가져오는 것은 일반적인 방법인 배열을 사용하는 것과 큰 차이가 없습니다 ㅋㅋㅋ
하지만 NSFetchResultsController의 가장 큰 장점은 이것이 아닙니다.
** NSFetchedResultsControllerDelegate를 사용하자 **
NSFetchedResultsControllerDelegate가 무엇이냐면 아래와 같다고 합니다 ㅎㅎㅎ
그러니깐 fetch result가 변경 될 때 fetch results controller와 관련된 것들을 묘사해주는 방법의 위임자 프로토콜이라 합니다 ㅎㅎ
쉽게 말해서 CoreDat의 CRUD가 발생하면 자동 호출을 해주는 것을 의미합니다.
위에서 정의한 것 처럼 fetchResultsController를 정의 하도록 하겠습니다.
//NSFetchedResultsControllerDelegate 상속받음
class ChatViewController: UIViewController , NSFetchedResultsControllerDelegate{
// self 객체를 사용하기 때문에 당연히 lazy
lazy var fetchResultController : NSFetchedResultsController<Message> = {
let fetchRequest : NSFetchRequest<Message> = Message.fetchRequest()
let sort = NSSortDescriptor(key: "date", ascending: true)
let filter = NSPredicate(format: "chat_friend.name = %@", self.friend!.name!)
fetchRequest.sortDescriptors = [sort]
fetchRequest.predicate = filter
let fetchResult = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: cdControl.context, sectionNameKeyPath: nil, cacheName: nil)
//이 녀석이 추가 됨
fetchResult.delegate = self
return fetchResult
}()
,,,
}
여기서 달라진 것은
일단 NSFetchedResultsControllerDelegate를 상속 받았다는 것이고
fetchResultController에 fetchResult.delegate = self 로 해주었습니다.
이게 무슨 말인고 하면 fetchResult.delegate에서 수행할 일을 self인 내가 대신 해줄게를 의미합니다.
이렇게 해줬으면 다음엔 함수를 호출 해줍니다.
// NSFetchedResultsControllerDelegate를 처리한 후에
// 이 didChange anObject 함수를 호출하면 CoreData가 변화 할 때 마다 호출 된다.
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == .insert {
delegate?.sendUpdated()
chatcollectionView.insertItems(at: [newIndexPath!])
chatcollectionView.scrollToItem(at: newIndexPath!, at: .bottom, animated: true)
}
}
controllerdidChange 함수를 호출해줍니다.
이 함수는 CoreData에 데이터가 CRUD가 발생하면 호출 되는 함수 입니다.
총 이렇게 4가지가 있습니다. (삭제가 발생 할 때, 입력이 될 때, 데이터 위치가 변할 때, 수정 될 때 )
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .update:
let cell = tableView.cellForRow(at: indexPath!) as! TeamCell
configure(cell: cell, for: indexPath!)
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
@unknown default:
print("Unexpected NSFetchedResultsChangeType")
}
그리고 이렇게 사용 될 수 있습니다.
type : 어떤 상태일 때 (Insert , update, delete, move)
indexPath : fetchResultsController의 이전의 위치
newIndexPath : fetchResultsController의 새로운 위치
** 다른 View에서 CoreData 변화가 생긴 것을 이전 뷰에 적용하고 싶을 때 **
만약에 A와 B의 뷰가 있고 (A -> B)와 같은 상태 일 때,
B에서 fetchResultsController에 변화가 생겼고 이것을 A 뷰에도 알리고 싶을 때는
Delegate Pattern을 이용해줍니다.
자세한 것은 아래 링크를 타고 가면 됩니다.
2020/11/04 - [Xcode/Swift - PlayGround] - PlayGround) Delegate와 Delegate Data Pass를 알아보자!
예를 들어 MainViewController가 있고 ChatViewController가 있다고 할 때,
(MainViewController => ChatViewController)라고 합시다 ㅎㅎ
//프로토콜 설정
protocol SendUpdateProtocol : class {
func sendUpdated()
}
이렇게 프로토콜을 설정 해주고
ChatViewController에 delegate를 설정해 줍니다.
class ChatViewController: UIViewController , NSFetchedResultsControllerDelegate{
weak var delegate : SendUpdateProtocol?
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == .insert {
//BlockOperation은 비동기적인 작업이기 때문에 이렇게 넣어주고
blockOperations.append(BlockOperation(block: {
self.chatcollectionView.insertItems(at: [newIndexPath!])
}))
delegate?.sendUpdated()
}
}
}
이렇게 되면 controller에 insert가 수행 되면 delegate.sendUpdate() 함수를 수행하게 되는데,
이 delegate는 누구로 위임 되어 있냐하면,
class MainViewController: UIViewController , NSFetchedResultsControllerDelegate, SendUpdateProtocol{
func sendUpdated() {
click()
}
@objc private func click(){
do {
try fetchResultController.performFetch()
mainCollectionView.reloadData()
}
catch let err{
print(err.localizedDescription)
}
}
}
extension MainViewController : UICollectionViewDelegate , UICollectionViewDataSource {
,,,, 생략 ,,,,
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let test = fetchResultController.object(at: indexPath)
let nextVC = ChatViewController()
nextVC.friend = test
//너의 일을 내가 대신 해줄게
nextVC.delegate = self
navigationController?.pushViewController(nextVC, animated: true)
}
}
이렇게 되어 있고 nextVC.delegate = self 를 통해
ChatVeiewController의 deleate가 할 일을 MainViewController가 대신 하겠다고 해줍니다.
그러면 위임 받은 sendUpdate()함수가 수행이 되고 연이어 click 함수가 수행이 됩니다.
이때, 주의 할 것이 그냥 reloadData()를 수행 해주면 안되고 항상!!!
do {
try fetchResultController.performFetch()
mainCollectionView.reloadData()
}
catch let err{
print(err.localizedDescription)
}
fetchResultController를 다시 performFetch 해줘서 새롭게 가져온 다음 reloadData()를 해줘야 합니다.
** 여러개의 데이터를 동시에 fetch 했을 때 오류를 해결하는 방법 **
만약에 CoreData에 한번에 여러개의 데이터를 수정하게 되면
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == .insert {
chatcollectionView.insertItems(at: [newIndexPath!])
chatcollectionView.scrollToItem(at: newIndexPath!, at: .bottom, animated: true)
}
}
이 함수를 수행 할때, CollectionView는 한번에 여러개의 데이터를 수행 해줘야 하기 때문에 어느쪽 Index에 insertItem을 해줘야 할지
헷갈리게 됩니다.
이럴 때는 BlockOperation을 수행 해줘야합니다. (아직 Multi Thread 부분이 약해서,,,)
var blockOperations : [BlockOperation] = []
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == .insert {
//BlockOperation은 비동기적인 작업이기 때문에 이렇게 넣어주고
blockOperations.append(BlockOperation(block: {
self.chatcollectionView.insertItems(at: [newIndexPath!])
}))
delegate?.sendUpdated()
}
}
이렇게 BlockOperation을 수행하면 됩니다.
자세한 것은 잘 모르지만 BlockOperation을 차례대로 수행하기 때문에 이렇게 새롭게 들어 온 것을 하나씩 BlockOperation 배열에 담아 줍니다.
그리고 이것을 새로운 fetchResultsControllerDeleate 함수에서 수행하게 해줍니다.
//CoreData에 변화가 감지 됐을 때, 작동하는 것
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
//CollectionView에 여러가지 작업을 수행 할 때 사용
chatcollectionView.performBatchUpdates {
//각 작업을 수행 해준다.
for item in blockOperations {
item.start()
}
} completion: { (_) in
let lastItem = self.fetchResultController.sections![0].numberOfObjects - 1
let indexpath = IndexPath(item: lastItem, section: 0)
self.chatcollectionView.scrollToItem(at: indexpath, at: .bottom, animated: true)
}
}
이렇게 수행 해줍니다.
이부분은 나중에 다시 자세히 다루도록 하겠습니다. ㅠㅠ
이렇게 fetchResult에 대해 알아봤습니다!!
아직 부족한 것이 많지만 나중에 더 추가할 내용이 있다고 더 추가 하도록 하겠습니다.
참고 사이트 :
NSFetchResultsController에 대한 기본적인 내용 :
NSFetchResultsController에 대한 자세한 사용 방법 :
NSfetchResultContoller와 reloadData를 함께 사용하는 방법 :
stackoverflow.com/a/19906166/13065642
CoreData 사용 방법에 대한 apple 공식 문서 :
NSFetchedResultsContoller 내용 :
developer.apple.com/documentation/coredata/nsfetchedresultscontroller
NSFetchedResultsControllerDelegate
developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate
'Xcode > IOS' 카테고리의 다른 글
IOS)Rx의 Publish와 Subscribe를 MVVM으로 구현해보기 (0) | 2021.09.22 |
---|---|
IOS) Literal에 대해서 알아보자! (0) | 2021.01.31 |
IOS) CollectionView에서 특정 Cell에 내용 넣기 In Code (0) | 2021.01.09 |
IOS) 동적인 Collection Cell 크기 만들기 - (부정확) (0) | 2020.12.31 |
IOS) Custom TabBar 만들기 in Code (0) | 2020.12.29 |
댓글