0

Core Data In Memory Store

 2 years ago
source link: https://useyourloaf.com/blog/core-data-in-memory-store/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Speed up your tests and SwiftUI previews by creating your Core Data stack with an in-memory store.

The Core Data stack uses an on-disk SQLite store by default. That causes some extra work when running tests as you need to reset the store to a known state before each test. It’s also slow which is a pain if you’re working with SwiftUI previews.

In Memory Store

The old way of creating an in-memory store was to change the store type in the persistent store descriptor before loading the store. The default is NSSQLiteStoreType but we can switch to NSInMemoryStoreType:

storeDescription.type = NSInMemoryStoreType

There’s nothing I can find in the documentation but Apple showed a different way during WWDC 2018. I was only reminded of it when looking at the project templates after creating a new Xcode Core Data project. The trick is to set the URL of the persistent store description to /dev/null before loading the store:

storeDescription.url = URL(fileURLWithPath: "/dev/null")

This still uses an SQLite store but we keep it in memory instead of writing it to disk. As well as being faster this also gives us a clean store each time.

NSPersistentContainer

I’m using a subclass of NSPersistentContainer to load and configure my Core Data store. That’s a convenient place to create a custom initializer that has an option to use an in memory store:

public final class CoreDataContainer: NSPersistentContainer {
  public init(name: String, bundle: Bundle = .main, inMemory: Bool = false) {
    guard let mom = NSManagedObjectModel.mergedModel(from: [bundle]) else {
      fatalError("Failed to create mom")
    }
    super.init(name: name, managedObjectModel: mom)
    configureDefaults(inMemory)
  }
}

Then when I configure the store I change the url of the store description:

private func configureDefaults(_ inMemory: Bool = false) {
  if let storeDescription = persistentStoreDescriptions.first {
    storeDescription.shouldAddStoreAsynchronously = true        
    if inMemory {
      storeDescription.url = URL(fileURLWithPath: "/dev/null")
      storeDescription.shouldAddStoreAsynchronously = false
    }
  }
}

Note that I default to loading my store asynchronously to allow for slow store migrations. I don’t need to worry about that with an in-memory store. Since the store is always empty I can switch back to adding the store synchronously which keeps thing simple.

Unit Testing

In my unit tests I can now create the core data container with an in-memory store:

final class CoreDataContainerTests: XCTestCase {
  private var container: CoreDataContainer!

  override func setUpWithError() throws {
    let container = CoreDataContainer(name: "Model", inMemory: true)
    container.loadPersistentStores { description, error in
      XCTAssertNil(error)
    }
  }
    
  override func tearDownWithError() throws {
    container = nil
  }
    
  func testPerformRequest() throws {
    let context = container.viewContext
    ...
  }
}

Note: I still keep some tests that use an on-disk store to reproduce the app environment.

SwiftUI Previews

This approach also works great for SwiftUI previews. I’ve been experimenting with keeping my Core Data objects contained in a store that publishes updates with Combine:

public final class WorldStore: ObservableObject {
  @Published public private(set) var countries = [Country]()
  @Published public private(set) var error: Error?

I’m creating the core data container when I initialize the store so I can again include an option for an in-memory store:

  private let dataContainer: CoreDataContainer

  public init(inMemory: Bool = false) {
    dataContainer = CoreDataContainer(name: "World", inMemory: inMemory)
    dataContainer.loadPersistentStores { ... }
  }
}

I add my SwiftUI preview data as development assets:

extension Country {
  static var previewData = [...]
}

I follow a similar approach to create a preview version of my store that is pre-populated with sample data:

extension WorldStore {
  static var preview: WorldStore = {
    let store = WorldStore(inMemory: true)
    store.update(Country.previewData)
    return store
  }()
}

I can then use this in my SwiftUI previews:

struct WorldView_Previews: PreviewProvider {
  static var previews: some View {
    WorldView()
      .environmentObject(WorldStore.preview)
  }
}

Further Details


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK