├── .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 | [](https://swift.org/package-manager)
4 | [](https://github.com/Alexander-Ignition/CombineCoreData/blob/master/LICENSE)
5 | [](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