본문 바로가기
Xcode/IOS

IOS) NSFetchedResultsController을 이용하자!

by 후르륵짭짭 2021. 1. 12.
728x90
반응형

안녕하세요! 후르륵짭짭 입니다.

이번에는 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를 알아보자!

 

PlayGround) Delegate와 Delegate Data Pass를 알아보자!

안녕하세요! 후르륵짭짭 입니다. 이번에는 Delegate와 Delegate를 활용해서 ViewController 간의 데이터 전송 방법을 배워보려 합니다. ViewController간에 DataPass 방법에는 총 3가지가 있습니다. 1. Notificat..

hururuek-chapchap.tistory.com

예를 들어 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에 대한 기본적인 내용 :

www.hackingwithswift.com/read/38/10/optimizing-core-data-performance-using-nsfetchedresultscontroller

 

Optimizing Core Data Performance using NSFetchedResultsController - a free Hacking with Swift tutorial

Was this page useful? Let us know! 1 2 3 4 5

www.hackingwithswift.com

NSFetchResultsController에 대한 자세한 사용 방법 : 

www.raywenderlich.com/books/core-data-by-tutorials/v7.0/chapters/5-nsfetchedresultscontroller#toc-chapter-008-anchor-005

 

NSFetchedResultsController

Table views are at the core of many iOS apps, and Apple wants to make Core Data play nicely with them! In this chapter, you’ll learn how NSFetchedResultsController can save you time and code when your table views are backed by data from Core Data.

www.raywenderlich.com

NSfetchResultContoller와 reloadData를 함께 사용하는 방법 :

stackoverflow.com/a/19906166/13065642

 

NSFetchedResultsController refresh for fetching new data

Please direct me to the right way. I implemented this code to fetch my objects: - (NSFetchedResultsController *)fetchedResultsController { if (_fetchedResultsController != nil) { return

stackoverflow.com

CoreData 사용 방법에 대한 apple 공식 문서 : 

developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/HowManagedObjectsarerelated.html

 

Core Data Programming Guide: Creating Managed Object Relationships

Core Data Programming Guide

developer.apple.com

NSFetchedResultsContoller 내용 : 

developer.apple.com/documentation/coredata/nsfetchedresultscontroller

 

Apple Developer Documentation

 

developer.apple.com

NSFetchedResultsControllerDelegate

developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate

 

Apple Developer Documentation

 

developer.apple.com

 

728x90
반응형

댓글