관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] CoreData 사용 (protocol 이용, in-memory 개념) 본문

iOS 응용 (swift)

[iOS - swift] CoreData 사용 (protocol 이용, in-memory 개념)

jake-kim 2020. 11. 23. 15:28

기본개념은 여기 참고

DataModel추가

  • 새로 만들기에서 Data Model추가

  • 완성된 화면

  • "Add Entity"눌러서 엔터티 추가: 중요한 것은 Data형태의 updateDate를 추가
    (이 데이터는 나중에 addData할 때 같은 데이터가 있으면 date만 업데이트하는 용도로 사용 될 것)

  • Entity이름 수정

  • Codegen을 Menual/None으로 변경: Class Definition으로 두면 오류 발생
    "Mutiple commands produce '...' "

  • 생성된 .xcdatamodeld 클릭 > Xcode의 menu바에서 Editor -> "Create NSMagedObject Subclass"선택하여 클래스 생성

  • 두 가지 파일 생성 완료: Person+CoreDataClass, Person+CoreDataProperties

자동으로 만들어진 extensoin Person

데이터 Layer

  • Domain은 "-Store"라는 이름
  • 구현체는 "-Repotory"라는 이름 사용하여, 사용할 경우 형태는 Store지만 주입하는 객체는 "-Repository"를 주입하여 사용

데이터가 저장될 자료구조 CoreDataStack구현 (싱글턴)

  • 가장 핵심은, Container와 Context개념
  • Container란 데이터 공간을 참조
  • Context란 데이터 공간에서 CRUD하는 객체
// Domain

import Foundation
import CoreData

enum StoreType {
    case persistent, inMemory

    func NSStoreType() -> String {
        switch self {
        case .persistent:
            return NSSQLiteStoreType
        case .inMemory:
            return NSInMemoryStoreType
        }
    }
}

class CoreDataStack {
    static let shared = CoreDataStack(storeType: .persistent)

    let storeType: StoreType

    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "Person")

        if self.storeType == .inMemory {
            let description = NSPersistentStoreDescription()
            description.type = self.storeType.NSStoreType()
            container.persistentStoreDescriptions = [description]
        }

        container.loadPersistentStores { (_, error) in
            if let error = error as NSError? {
                fatalError("Unable to load core data persistent stores: \(error)")
            }
        }

        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // in-memory와 영구 저장소 merge 충돌: in-memory우선
        container.viewContext.shouldDeleteInaccessibleFaults = true // 접근 불가의 결함들을 삭제할 수 있게끔 설정
        container.viewContext.automaticallyMergesChangesFromParent = true // parent의 context가 바뀌면 자동으로 merge되는 설정

        return container
    }()

    var viewContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }

    init(storeType: StoreType) {
        self.storeType = storeType
    }

    fileprivate func setBackgroundContext(_ context: NSManagedObjectContext) {
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // in-memory와 영구 저장소 merge 충돌: in-memory우선
        context.undoManager = nil // nil인 경우, 실행 취소를 비활성화 (iOS에 디폴트값은 nil, macOS에서는 기본적으로 제공)
    }

    func taskContext() -> NSManagedObjectContext {
        let taskContext = persistentContainer.newBackgroundContext()
        setBackgroundContext(taskContext)

        return taskContext
    }

    func performBackgroundTask(task: @escaping (NSManagedObjectContext) -> Void) {
        persistentContainer.performBackgroundTask { (context) in
            self.setBackgroundContext(context)
            task(context)
        }
    }
}

"-Store"정의

// Domain

import CoreData

public protocol PersonModel {
    var id: String { get }
    var name: String { get }
}

public protocol PersonStore {
    func add(id: String, name: String)
    func remove(id: String, name: String)
    func removeAll()
    func count() -> Int?
    func removeLast()
}

"-Repository"정의

import CoreData

class PersonModel: PersonModelStore {
    var id: String = ""
    var name: String = ""

    init(id: String, name: String) {
        self.id = id
        self.name = name
    }
}

class PersonRepository: PersonStore {
    let coreDataStack: CoreDataStack
    let maxCount: Int

    init(coreDataStack: CoreDataStack = CoreDataStack.shared, maxCount: Int = 10) {
        self.coreDataStack = coreDataStack
        self.maxCount = maxCount
    }

    func add(id: String, name: String) {
        let context = coreDataStack.taskContext()

        if let count = count(), count == maxCount {
            removeLast()
        }

        if let savedPlace = fetch(id, name, in: context) {
            savedPlace.updateDate = Date()
        } else {
            create(id, name, in: context)
        }

        context.performAndWait {
            do {
                try context.save()
            } catch {
                print("addPlace error: \(error)")
            }
        }
    }

    func remove(id: String, name: String) {
        let context = coreDataStack.taskContext()
        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "(id.length > 0 AND id == %@) OR (name == %@)", argumentArray: [id, name])

        do {
            let objects = try context.fetch(fetchRequest)
            for object in objects {
                context.delete(object)
            }
            try context.save()
        } catch _ {
            // error handling
        }
    }

    fileprivate func fetch(_ id: String, _ name: String, in context: NSManagedObjectContext) -> Person? {
        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "(id.length > 0 AND id == %@) OR (name == %@)", argumentArray: [id, name])
        do {
            return try context.fetch(fetchRequest).first
        } catch {
            print("fetch for update Person error: \(error)")
            return nil
        }
    }

    fileprivate func create(_ id: String, _ name: String, in context: NSManagedObjectContext) {
        let place = Person(context: context)
        place.id = id
        place.name = name
        place.updateDate = Date()
    }

    func removeAll() {
        let context = coreDataStack.taskContext()
        let deleteRequest = NSBatchDeleteRequest(fetchRequest: Person.fetchRequest())

        do {
            try context.execute(deleteRequest)
            try context.save()
        } catch {
            print("removeAll Person error: \(error)")
        }
    }

    func getPersons() -> [PersonModel] {
        return fetchAll().map {
            return PersonModel(id: $0.id ?? "1", name: $0.name ?? "2")
        }
    }

    fileprivate func fetchAll() -> [Person] {
        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "updateDate", ascending: false)]

        do {
            return try coreDataStack.viewContext.fetch(fetchRequest)
        } catch {
            print("fetch Person error: \(error)")
            return []
        }
    }

    func count() -> Int? {
        let context = coreDataStack.taskContext()
        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
        do {
            let count = try context.count(for: fetchRequest)
            return count
        } catch {
            print("count of Person error: \(error)")
            return nil
        }
    }

    func removeLast() {
        guard let removeTarget = fetchAll().last,
              let id = removeTarget.id,
              let name = removeTarget.name else {
            return
        }
        remove(
            id: id,
            name: name
        )
    }
}

사용하는 쪽 예시)

  • Add를 누르면 코어 데이터 안에 데이터의 갯수를 불러와서 (count, count)로 데이터 만든 후 삽입
  • Delete를 누르면 코어 데이터 안에 removeLast하여 마지막 요소 삭제

영속성 데이터이므로 다시 실행해도 앱을 지우지 않는이상 데이터 존재

  • 사용하는 쪽의 주요 코드 샘플
import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    var dataSource = [PersonModel]()
    let personUseCase = PersonRepository() // 실제로 사용할 땐 ViewModel에서 주입하는 형태로 사용

    @IBOutlet weak var tbl: UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        tbl.delegate = self
        tbl.dataSource = self
        tbl.register(UITableViewCell.self, forCellReuseIdentifier: "TableViewCell")
    }

    @IBAction func btnDelete(_ sender: Any) {
        personUseCase.removeLast()
        dataSource = personUseCase.getPersons()
        tbl.reloadData()
    }

    @IBAction func btnAdd(_ sender: Any) {
        let count = String(describing: personUseCase.count() ?? 0)
        personUseCase.add(id: count, name: count)
        dataSource = personUseCase.getPersons()
        tbl.reloadData()
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell")! as UITableViewCell
        cell.textLabel?.text = dataSource[indexPath.row].id + ", " + dataSource[indexPath.row].name
        return cell
    }
}

소스코드: github.com/JK0369/CoredataEx

Comments