├── .github └── workflows │ └── test.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── CombineCoreData.xcscheme ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Books │ ├── Book.swift │ ├── BookStorage.swift │ └── Schema.swift └── CombineCoreData │ ├── PerformPublisher.swift │ └── Scheduler.swift └── Tests └── CombineCoreDataTests ├── Helpers └── Publisher+Wait.swift ├── PerformPublisherTests.swift ├── SchedulerTests.swift └── TestCase.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags-ignore: 7 | - '**' 8 | pull_request: 9 | branches: 10 | - '*' 11 | 12 | jobs: 13 | test: 14 | name: Run tests 15 | runs-on: macOS-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Build and test 20 | run: swift test --enable-code-coverage --disable-automatic-resolution 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/CombineCoreData.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 62 | 68 | 69 | 70 | 72 | 73 | 74 | 75 | 76 | 77 | 87 | 88 | 94 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexander Ignition 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CombineCoreData", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | .tvOS(.v13), 12 | .watchOS(.v6) 13 | ], 14 | products: [ 15 | .library( 16 | name: "CombineCoreData", 17 | targets: ["CombineCoreData"]), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "CombineCoreData", 22 | dependencies: []), 23 | .target( 24 | name: "Books", 25 | dependencies: ["CombineCoreData"]), 26 | .testTarget( 27 | name: "CombineCoreDataTests", 28 | dependencies: ["CombineCoreData", "Books"]), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚜 CombineCoreData 🗄 2 | 3 | [![SPM compatible](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager) 4 | [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/Alexander-Ignition/CombineCoreData/blob/master/LICENSE) 5 | [![GitHub Workflow Test](https://github.com/Alexander-Ignition/CombineCoreData/workflows/Test/badge.svg)](https://github.com/Alexander-Ignition/CombineCoreData/actions?query=workflow%3ATest) 6 | 7 | > Inspired by [ReactiveCocoa and Core Data Concurrency](https://thoughtbot.com/blog/reactive-core-data) 8 | 9 | - You will no longer need to use method `perform(_:)` directly with `do catch`. 10 | - You can forget about the callback based api when working with CoreData. 11 | 12 | ## Features 13 | 14 | - [x] NSManagedObjectContext produce Publisher 15 | - [x] NSManagedObjectContext + Scheduler 16 | 17 | 18 | ## Instalation 19 | 20 | Add dependency to `Package.swift`... 21 | 22 | ```swift 23 | .package(url: "https://github.com/Alexander-Ignition/CombineCoreData", from: "0.0.3"), 24 | ``` 25 | 26 | ... and your target 27 | 28 | ```swift 29 | .target(name: "ExampleApp", dependencies: ["CombineCoreData"]), 30 | ``` 31 | 32 | ## Usage 33 | 34 | Wrap any operation with managed objects in context with method `publisher(_:)`. 35 | 36 | ```swift 37 | import CombineCoreData 38 | 39 | managedObjectContext.publisher { 40 | // do something 41 | } 42 | ``` 43 | 44 | Full examples you can see in [Sources/Books](Sources/Books). This module contains [Book](Sources/Books/Book.swift) and [BookStorage](Sources/Books/BookStorage.swift) that manages books. 45 | 46 | ### Save objects 47 | 48 | Example of asynchronously saving books in а `backgroundContex` on its private queue. 49 | 50 | ```swift 51 | func saveBooks(names: [String]) -> AnyPublisher { 52 | backgroundContex.publisher { 53 | for name in names { 54 | let book = Book(context: self.backgroundContex) 55 | book.name = name 56 | } 57 | try self.backgroundContex.save() 58 | } 59 | } 60 | ``` 61 | 62 | ### Fetch objects 63 | 64 | Example of asynchronously fetching books in а `backgroundContex` on its private queue. 65 | 66 | ```swift 67 | func fetchBooks() -> AnyPublisher<[Book], Error> { 68 | backgroundContex.fetchPublisher(Book.all) 69 | } 70 | ``` 71 | 72 | ## Scheduler 73 | 74 | You can use `NSManagedObjectContext` instead of `OperationQeue`, `DispatchQueue` or `RunLoop` with operators `receive(on:)` and `subscribe(on:)` 75 | 76 | ```swift 77 | let subscription = itemService.load() 78 | .receive(on: viewContext) 79 | .sink(receiveCompletion: { completion in 80 | // Receive `completion` on main queue in `viewContext` 81 | print(completion) 82 | }, receiveValue: { (items: [Item]) in 83 | // Receive `[Item]` on main queue in `viewContext` 84 | print(book) 85 | }) 86 | ``` 87 | 88 | `CombineCoreData` extends `NSManagedObjectContext` to adapt the `Scheduler` protocol. Because `NSManagedObjectContext` has a private queue and and schedule task through method `perform(_:)`. 89 | -------------------------------------------------------------------------------- /Sources/Books/Book.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | 3 | @objc(Book) 4 | public final class Book: NSManagedObject { 5 | @NSManaged public var name: String? 6 | 7 | /// Use `try Book.all.execute()` for check context queue. 8 | public static var all: NSFetchRequest { 9 | let fetchRequest = NSFetchRequest(entityName: "Book") 10 | fetchRequest.sortDescriptors = [ 11 | NSSortDescriptor(keyPath: \Book.name, ascending: true) 12 | ] 13 | return fetchRequest 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Books/BookStorage.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import CoreData 3 | import CombineCoreData 4 | 5 | final class BookStorage { 6 | let backgroundContex: NSManagedObjectContext 7 | 8 | init(backgroundContex: NSManagedObjectContext) { 9 | self.backgroundContex = backgroundContex 10 | } 11 | } 12 | 13 | // MARK: - Save books 14 | 15 | extension BookStorage { 16 | 17 | func saveBooks(names: [String], completion: @escaping (Error?) -> Void) { 18 | backgroundContex.perform { 19 | for name in names { 20 | let book = Book(context: self.backgroundContex) 21 | book.name = name 22 | } 23 | do { 24 | try self.backgroundContex.save() 25 | completion(nil) 26 | } catch { 27 | completion(error) 28 | } 29 | } 30 | } 31 | 32 | func saveBooks(names: [String]) -> AnyPublisher { 33 | backgroundContex.publisher { 34 | for name in names { 35 | let book = Book(context: self.backgroundContex) 36 | book.name = name 37 | } 38 | try self.backgroundContex.save() 39 | } 40 | } 41 | } 42 | 43 | // MARK: - Fetch books 44 | 45 | extension BookStorage { 46 | 47 | func fetchBooks(completion: @escaping (Result<[Book], Error>) -> Void) { 48 | backgroundContex.perform { 49 | do { 50 | let books = try self.backgroundContex.fetch(Book.all) 51 | completion(.success(books)) 52 | } catch { 53 | completion(.failure(error)) 54 | } 55 | } 56 | } 57 | 58 | func fetchBooks() -> AnyPublisher<[Book], Error> { 59 | backgroundContex.fetchPublisher(Book.all) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Books/Schema.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | 3 | public enum Schema { 4 | /// Swift package manager not support *.xcdatamodel files. 5 | public static let model = NSManagedObjectModel().apply { 6 | $0.entities = [book] 7 | } 8 | 9 | private static let book = NSEntityDescription().apply { 10 | $0.name = "Book" 11 | $0.managedObjectClassName = $0.name 12 | $0.properties = [ 13 | NSAttributeDescription().apply { 14 | $0.attributeType = .stringAttributeType 15 | $0.name = "name" 16 | } 17 | ] 18 | } 19 | } 20 | 21 | extension NSObjectProtocol { 22 | func apply(configure: (Self) -> Void) -> Self { 23 | configure(self) 24 | return self 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/CombineCoreData/PerformPublisher.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import CoreData 3 | 4 | // MARK: - NSManagedObjectContext + PerformPublisher 5 | 6 | extension NSManagedObjectContext { 7 | /// Asynchronously performs a given `block` on the context’s queue. 8 | /// 9 | /// let backgroundContext: NSManagedObjectContext = // ... 10 | /// 11 | /// backgroundContext.publisher { () -> Book in 12 | /// let book = Book(context: backgroundContext) 13 | /// book.name = "CoreData" 14 | /// try backgroundContext.save() 15 | /// return book 16 | /// }.sink(receiveCompletion: { completion in 17 | /// print(completion) 18 | /// }, receiveValue: { (book: Book) in 19 | /// print(book) 20 | /// }) 21 | /// 22 | /// - Parameter block: CoreData operations. 23 | /// - Returns: Publisher of subscriptions that execute in the context. 24 | public func publisher( 25 | _ block: @escaping () throws -> T 26 | ) -> AnyPublisher { 27 | PerformPublisher( 28 | managedObjectContext: self, 29 | block: block 30 | ).eraseToAnyPublisher() 31 | } 32 | 33 | /// Asynchronously performs the fetch request on the context’s queue. 34 | /// 35 | /// let backgroundContext: NSManagedObjectContext = // ... 36 | /// let fetchRequest = NSFetchRequest(entityName: "Book") 37 | /// 38 | /// backgroundContext.fetchPublisher(fetchRequest) 39 | /// .sink(receiveCompletion: { completion in 40 | /// print(completion) 41 | /// }, receiveValue: { (books: [Book]) in 42 | /// print(books) 43 | /// }) 44 | /// 45 | /// - Parameter fetchRequest: A fetch request that specifies the search criteria for the fetch. 46 | /// - Returns: Publisher of subscriptions that execute in the context. 47 | public func fetchPublisher( 48 | _ fetchRequest: NSFetchRequest 49 | ) -> AnyPublisher<[T], Error> where T: NSFetchRequestResult { 50 | PerformPublisher<[T]>(managedObjectContext: self) { 51 | try fetchRequest.execute() 52 | }.eraseToAnyPublisher() 53 | } 54 | } 55 | 56 | // MARK: - Private 57 | 58 | /// A publisher that asynchronously performs a given `block` on the context’s queue. 59 | private struct PerformPublisher: Publisher { 60 | /// Untyped error is thrown from the `block`. 61 | typealias Failure = Error 62 | 63 | /// The context on which `block` will be executed. 64 | let managedObjectContext: NSManagedObjectContext 65 | 66 | /// A block to execute in `managedObjectContext`. 67 | let block: () throws -> Output 68 | 69 | func receive( 70 | subscriber: S 71 | ) where S: Subscriber, Failure == S.Failure, Output == S.Input { 72 | let subscription = PerformSubscription( 73 | subscriber: AnySubscriber(subscriber), 74 | publisher: self) 75 | subscriber.receive(subscription: subscription) 76 | } 77 | } 78 | 79 | private final class PerformSubscription: Subscription, CustomStringConvertible { 80 | private var subscriber: AnySubscriber? 81 | private let publisher: PerformPublisher 82 | var description: String { "PerformPublisher" } // for publisher print operator 83 | 84 | init(subscriber: AnySubscriber, 85 | publisher: PerformPublisher 86 | ) { 87 | self.subscriber = subscriber 88 | self.publisher = publisher 89 | } 90 | 91 | func request(_ demand: Subscribers.Demand) { 92 | guard demand != .none, subscriber != nil else { return } 93 | 94 | publisher.managedObjectContext.perform { 95 | guard let subscriber = self.subscriber else { return } 96 | do { 97 | let output = try self.publisher.block() 98 | _ = subscriber.receive(output) 99 | subscriber.receive(completion: .finished) 100 | } catch { 101 | subscriber.receive(completion: .failure(error)) 102 | } 103 | } 104 | } 105 | 106 | func cancel() { 107 | subscriber = nil 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/CombineCoreData/Scheduler.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import CoreData 3 | 4 | extension NSManagedObjectContext: Scheduler { 5 | 6 | public typealias SchedulerTimeType = ImmediateScheduler.SchedulerTimeType 7 | public typealias SchedulerOptions = ImmediateScheduler.SchedulerOptions 8 | 9 | public var now: SchedulerTimeType { 10 | ImmediateScheduler.shared.now 11 | } 12 | 13 | public var minimumTolerance: SchedulerTimeType.Stride { 14 | ImmediateScheduler.shared.minimumTolerance 15 | } 16 | 17 | public func schedule( 18 | options: SchedulerOptions?, 19 | _ action: @escaping () -> Void 20 | ) { 21 | perform(action) 22 | } 23 | 24 | public func schedule( 25 | after date: SchedulerTimeType, 26 | interval: SchedulerTimeType.Stride, 27 | tolerance: SchedulerTimeType.Stride, 28 | options: SchedulerOptions?, 29 | _ action: @escaping () -> Void 30 | ) -> Cancellable { 31 | perform(action) 32 | return AnyCancellable({ /* none */ }) 33 | } 34 | 35 | public func schedule( 36 | after date: SchedulerTimeType, 37 | tolerance: SchedulerTimeType.Stride, 38 | options: SchedulerOptions?, 39 | _ action: @escaping () -> Void 40 | ) { 41 | perform(action) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/CombineCoreDataTests/Helpers/Publisher+Wait.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import XCTest 3 | 4 | /// Result of subscribing to the publisher. 5 | /// 6 | /// Similar to `Swift.Result` and `Combine.Record`. 7 | public struct PublisherResult where Failure: Error { 8 | /// Published values. 9 | public var output: [Output] = [] 10 | 11 | /// Failure or finished completion result. 12 | public var completion: Subscribers.Completion = .finished 13 | 14 | /// Published error if failure. Use it for testing errors. 15 | /// 16 | /// let error = URLError(.notConnectedToInternet) 17 | /// let result = Fail(error: error).wait() 18 | /// XCTAssertEqual(result.values, []) 19 | /// XCTAssertEqual(result.error, error) 20 | /// 21 | public var error: Failure? { 22 | switch completion { 23 | case .failure(let error): 24 | return error 25 | case .finished: 26 | return nil 27 | } 28 | } 29 | 30 | /// Empty finished result. 31 | public init() {} 32 | 33 | /// Returns the output values as a throwing expression. 34 | /// 35 | /// let publisher = [1, 2, 3].publisher 36 | /// let numbers = try publisher.wait().get() // [1, 2, 3] 37 | /// 38 | /// let error = URLError(.networkConnectionLost) 39 | /// let record = Record(output: [1, 2], completion: .failure(error)) 40 | /// let numbers = try record.wait().get() // assert and error 41 | /// 42 | /// - Parameters: 43 | /// - file: The file in which the failure occurred. The default is the file name of the test case in which this function was called. 44 | /// - line: The line number on which the failure occurred. The default is the line number on which this function was called. 45 | /// - Throws: The error, if a publisher is failure. 46 | /// - Returns: The output values, if a publisher is successfully finished. 47 | public func get(file: StaticString = #file, line: UInt = #line) throws -> [Output] { 48 | switch completion { 49 | case .failure(let error): 50 | XCTFail("\(error)", file: file, line: line) 51 | throw error 52 | case .finished: 53 | return output 54 | } 55 | } 56 | 57 | /// Сheck that the result was completed successfully with a single value. 58 | /// 59 | /// try Just(4).wait().single() // 4 60 | /// try Empty().wait().single() // assert fail and error 61 | /// try [1, 2, 3].publisher.wait().single() // assert fail 62 | /// 63 | /// - Parameters: 64 | /// - file: The file in which the failure occurred. The default is the file name of the test case in which this function was called. 65 | /// - line: The line number on which the failure occurred. The default is the line number on which this function was called. 66 | /// - Throws: Publisher error or empty result error. 67 | /// - Returns: The first outgoing element of the publisher. 68 | public func single(file: StaticString = #file, line: UInt = #line) throws -> Output { 69 | let values = try get(file: file, line: line) 70 | XCTAssertEqual(values.count, 1, file: file, line: line) 71 | return try XCTUnwrap(values.first, file: file, line: line) 72 | } 73 | } 74 | 75 | extension Publisher { 76 | /// Wait for the publisher to complete. 77 | /// 78 | /// final class ExampleTests: XCTestCase { 79 | /// func testJust() { 80 | /// let publisher = Just("Hello") 81 | /// let string = try publisher.wait().single() 82 | /// XCTAssertEqual(string, "Hello") 83 | /// } 84 | /// } 85 | /// 86 | /// - Warning: Not thread safe! 87 | /// - Parameters: 88 | /// - timeout: The amount of time within which all expectations must be fulfilled. 89 | /// - description: A string to display in the test log for this expectation, to help diagnose failures. 90 | /// - file: The file in which the failure occurred. The default is the file name of the test case in which this function was called. 91 | /// - line: The line number on which the failure occurred. The default is the line number on which this function was called. 92 | /// - Returns: Result of subscribing to the publisher. 93 | public func wait( 94 | timeout: TimeInterval = 5, 95 | description: String = #function, 96 | file: StaticString = #file, 97 | line: UInt = #line 98 | ) -> PublisherResult { 99 | 100 | var result = PublisherResult() 101 | let expectation = XCTestExpectation(description: description) 102 | 103 | let subscription = sink(receiveCompletion: { completion in 104 | result.completion = completion 105 | expectation.fulfill() 106 | }, receiveValue: { value in 107 | result.output.append(value) 108 | }) 109 | let waiterResult = XCTWaiter.wait(for: [expectation], timeout: timeout) 110 | XCTAssertEqual(waiterResult, .completed, file: file, line: line) 111 | 112 | subscription.cancel() 113 | return result 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/CombineCoreDataTests/PerformPublisherTests.swift: -------------------------------------------------------------------------------- 1 | import Books 2 | import XCTest 3 | 4 | enum TestError: Error { 5 | case error 6 | } 7 | 8 | final class PerformPublisherTests: TestCase { 9 | 10 | func testFetchPublisher() throws { 11 | let savedIds = try saveBooks().map(\.objectID) 12 | 13 | let fetchedIds = try backgroundContext 14 | .fetchPublisher(Book.all) 15 | .wait() 16 | .single() 17 | .map(\.objectID) 18 | 19 | XCTAssertEqual(fetchedIds, savedIds) 20 | } 21 | 22 | func testPublisherWithBlock() throws { 23 | let savedIds = try saveBooks().map(\.objectID) 24 | 25 | let fetchedIds = try backgroundContext 26 | .publisher { try Book.all.execute() } 27 | .wait() 28 | .single() 29 | .map(\.objectID) 30 | 31 | XCTAssertEqual(fetchedIds, savedIds) 32 | } 33 | 34 | func testPublisherWithFailedBlock() { 35 | let result = viewContext 36 | .publisher { () -> Book in throw TestError.error } 37 | .wait() 38 | 39 | XCTAssertEqual(result.output, []) 40 | XCTAssertTrue(result.error is TestError) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/CombineCoreDataTests/SchedulerTests.swift: -------------------------------------------------------------------------------- 1 | import Books 2 | import Combine 3 | import XCTest 4 | 5 | final class SchedulerTests: TestCase { 6 | 7 | func testNow() { 8 | let scheduler = ImmediateScheduler.shared 9 | XCTAssertEqual(viewContext.now, scheduler.now) 10 | XCTAssertEqual(backgroundContext.now, scheduler.now) 11 | XCTAssertEqual(backgroundContext.now, viewContext.now) 12 | } 13 | 14 | func testMinimumTolerance() { 15 | let scheduler = ImmediateScheduler.shared 16 | XCTAssertEqual(viewContext.minimumTolerance, scheduler.minimumTolerance) 17 | XCTAssertEqual(backgroundContext.minimumTolerance, scheduler.minimumTolerance) 18 | XCTAssertEqual(backgroundContext.minimumTolerance, viewContext.minimumTolerance) 19 | } 20 | 21 | func testSubscribeOn() throws { 22 | XCTAssertNoThrow(try saveBooks()) 23 | 24 | let context = try Just(Book.all) 25 | .eraseToAnyPublisher() 26 | .tryMap { try $0.execute().first?.managedObjectContext } 27 | .subscribe(on: backgroundContext) 28 | .wait() 29 | .single() 30 | 31 | XCTAssertEqual(context, backgroundContext) 32 | } 33 | 34 | func testReceiveOn() throws { 35 | XCTAssertNoThrow(try saveBooks()) 36 | 37 | let context = try Just(Book.all) 38 | .eraseToAnyPublisher() 39 | .receive(on: backgroundContext) 40 | .tryMap { try $0.execute().first?.managedObjectContext } 41 | .wait() 42 | .single() 43 | 44 | XCTAssertEqual(context, backgroundContext) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/CombineCoreDataTests/TestCase.swift: -------------------------------------------------------------------------------- 1 | import Books 2 | import XCTest 3 | 4 | class TestCase: XCTestCase { 5 | private var container: NSPersistentContainer! 6 | var viewContext: NSManagedObjectContext { container.viewContext } 7 | private(set) var backgroundContext: NSManagedObjectContext! 8 | 9 | override func setUp() { 10 | super.setUp() 11 | container = NSPersistentContainer( 12 | name: "Books", 13 | managedObjectModel: Schema.model 14 | ) 15 | container.persistentStoreDescriptions.forEach { 16 | $0.type = NSInMemoryStoreType 17 | } 18 | container.loadPersistentStores { storeDescription, error in 19 | XCTAssertNil(error, "\(storeDescription)") 20 | } 21 | backgroundContext = container.newBackgroundContext() 22 | backgroundContext.name = "com.combine-coredata.tests.background-context" 23 | viewContext.name = "com.combine-coredata.tests.main-context" 24 | } 25 | 26 | @discardableResult 27 | func saveBooks(names: [String] = ["Combine", "CoreData"]) throws -> [Book] { 28 | let books = names.map { name -> Book in 29 | let book = Book(context: viewContext) 30 | book.name = name 31 | return book 32 | } 33 | try viewContext.save() 34 | return books 35 | } 36 | } 37 | --------------------------------------------------------------------------------