├── .DS_Store
├── Persistence
├── .DS_Store
├── CoreDataCloudKitShare.xcdatamodeld
│ ├── .xccurrentversion
│ └── CoreDataCloudKitShare.xcdatamodel
│ │ └── contents
├── PersistenceController+Rating.swift
├── PersistenceController+Photo.swift
├── CoreDataHelper.swift
├── PersistenceController+Deduplicate.swift
├── PersistenceController+Tag.swift
├── PersistenceController+History.swift
├── PersistenceController.swift
└── PersistenceController+Share.swift
├── CoreDataCloudKitShare
├── .DS_Store
├── Assets.xcassets
│ ├── Contents.json
│ ├── .DS_Store
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── InitializeCloudKitSchema-Info.plist
├── Info.plist
├── CoreDataCloudKitShare.entitlements
├── AppDelegate.swift
├── SceneDelegate.swift
├── CoreDataCloudKitShareApp.swift
└── PhotoPicker.swift
├── CoreDataCloudKitShareOnWatch
├── Assets.xcassets
│ ├── Contents.json
│ ├── .DS_Store
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── .DS_Store
└── Info.plist
├── CoreDataCloudKitShareOnWatch WatchKit Extension
├── Assets.xcassets
│ ├── Contents.json
│ └── Complication.complicationset
│ │ ├── Graphic Bezel.imageset
│ │ └── Contents.json
│ │ ├── Graphic Circular.imageset
│ │ └── Contents.json
│ │ ├── Graphic Corner.imageset
│ │ └── Contents.json
│ │ ├── Graphic Large Rectangular.imageset
│ │ └── Contents.json
│ │ ├── Modular.imageset
│ │ └── Contents.json
│ │ ├── Circular.imageset
│ │ └── Contents.json
│ │ ├── Extra Large.imageset
│ │ └── Contents.json
│ │ ├── Utilitarian.imageset
│ │ └── Contents.json
│ │ ├── Graphic Extra Large.imageset
│ │ └── Contents.json
│ │ └── Contents.json
├── .DS_Store
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── CoreDataCloudKitShareOnWatch WatchKit Extension.entitlements
├── CoreDataCloudKitShareApp.swift
├── Info.plist
└── ExtensionDelegate.swift
├── CoreDataCloudKitShare.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
├── xcshareddata
│ └── xcschemes
│ │ ├── InitializeCloudKitSchema.xcscheme
│ │ ├── CoreDataCloudKitShare.xcscheme
│ │ └── CoreDataCloudKitShareOnWatch.xcscheme
└── project.pbxproj
├── SwiftUI
├── SwiftUIHelper.swift
├── FullImageView.swift
├── AddToExistingShareView.swift
├── PhotoGridItemView.swift
├── ManagingSharesView.swift
├── SharePickerView.swift
├── RatingView.swift
├── PhotoContextMenu.swift
├── TaggingView.swift
├── PhotoGridView.swift
└── ParticipantView.swift
├── LICENSE
├── .gitignore
└── README.md
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delawaremathguy/CoreDataCloudKitShare/HEAD/.DS_Store
--------------------------------------------------------------------------------
/Persistence/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delawaremathguy/CoreDataCloudKitShare/HEAD/Persistence/.DS_Store
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delawaremathguy/CoreDataCloudKitShare/HEAD/CoreDataCloudKitShare/.DS_Store
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delawaremathguy/CoreDataCloudKitShare/HEAD/CoreDataCloudKitShareOnWatch/.DS_Store
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/Assets.xcassets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delawaremathguy/CoreDataCloudKitShare/HEAD/CoreDataCloudKitShare/Assets.xcassets/.DS_Store
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch/Assets.xcassets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delawaremathguy/CoreDataCloudKitShare/HEAD/CoreDataCloudKitShareOnWatch/Assets.xcassets/.DS_Store
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delawaremathguy/CoreDataCloudKitShare/HEAD/CoreDataCloudKitShareOnWatch WatchKit Extension/.DS_Store
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CKSharingSupported
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildSystemType
6 | Latest
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Persistence/CoreDataCloudKitShare.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | CoreDataCloudKitShare.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/InitializeCloudKitSchema-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIBackgroundModes
6 |
7 | remote-notification
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CKSharingSupported
6 |
7 | UIBackgroundModes
8 |
9 | remote-notification
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : ">183"
11 | }
12 | ],
13 | "info" : {
14 | "author" : "xcode",
15 | "version" : 1
16 | },
17 | "properties" : {
18 | "auto-scaling" : "auto"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : ">183"
11 | }
12 | ],
13 | "info" : {
14 | "author" : "xcode",
15 | "version" : 1
16 | },
17 | "properties" : {
18 | "auto-scaling" : "auto"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : ">183"
11 | }
12 | ],
13 | "info" : {
14 | "author" : "xcode",
15 | "version" : 1
16 | },
17 | "properties" : {
18 | "auto-scaling" : "auto"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : ">183"
11 | }
12 | ],
13 | "info" : {
14 | "author" : "xcode",
15 | "version" : 1
16 | },
17 | "properties" : {
18 | "auto-scaling" : "auto"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/CoreDataCloudKitShare.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.icloud-container-identifiers
8 |
9 | iCloud.com.example.ziqiao-samplecode.CoreDataCloudKitShare
10 |
11 | com.apple.developer.icloud-services
12 |
13 | CloudKit
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/CoreDataCloudKitShareOnWatch WatchKit Extension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.icloud-container-identifiers
8 |
9 | iCloud.com.example.ziqiao-samplecode.CoreDataCloudKitShare
10 |
11 | com.apple.developer.icloud-services
12 |
13 | CloudKit
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/CoreDataCloudKitShareApp.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | The SwiftUI app for watchOS.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 |
11 | @main
12 | struct CoreDataCloudKitShareApp: App {
13 | @WKExtensionDelegateAdaptor var delegateOfExtension: ExtensionDelegate
14 |
15 | let persistenceController = PersistenceController.shared
16 |
17 | @SceneBuilder var body: some Scene {
18 | WindowGroup {
19 | PhotoGridView()
20 | .environment(\.managedObjectContext, persistenceController.persistentContainer.viewContext)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | NSExtension
8 |
9 | NSExtensionAttributes
10 |
11 | WKAppBundleIdentifier
12 | com.example.ziqiao-samplecode.CoreDataCloudKitShare.watchkitapp
13 |
14 | NSExtensionPointIdentifier
15 | com.apple.watchkit
16 |
17 | UIBackgroundModes
18 |
19 | remote-notification
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | The app delegate class.
5 |
6 |
7 | */
8 |
9 | import UIKit
10 | import CoreData
11 |
12 | //@main
13 | class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | return true
16 | }
17 |
18 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession,
19 | options: UIScene.ConnectionOptions) -> UISceneConfiguration {
20 | let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
21 | configuration.delegateClass = SceneDelegate.self
22 | return configuration
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SwiftUI/SwiftUIHelper.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | Extensions that add convenience methods to SwiftUI.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import Combine
11 |
12 | extension Color {
13 | static var listHeaderBackground: Color {
14 | #if os(iOS)
15 | return Color(uiColor: .systemGroupedBackground)
16 | #elseif os(watchOS)
17 | return Color(uiColor: .clear)
18 | #endif
19 | }
20 |
21 | static var gridItemBackground: Color {
22 | #if os(iOS)
23 | return Color(.systemGray6)
24 | #elseif os(watchOS)
25 | return Color.gray
26 | #endif
27 | }
28 | }
29 |
30 | extension NotificationCenter {
31 | var storeDidChangePublisher: Publishers.ReceiveOn {
32 | return publisher(for: .cdcksStoreDidChange).receive(on: DispatchQueue.main)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/ExtensionDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | The WatchKit extension delegate class.
5 |
6 |
7 | */
8 |
9 | import WatchKit
10 | import CloudKit
11 |
12 | class ExtensionDelegate: NSObject, WKExtensionDelegate {
13 | /**
14 | To be able to accept a share, add a CKSharingSupported entry in the info.plist file of the WatchKit app and set it to true.
15 | */
16 | func userDidAcceptCloudKitShare(with cloudKitShareMetadata: CKShare.Metadata) {
17 | let persistenceController = PersistenceController.shared
18 | let sharedStore = persistenceController.sharedPersistentStore
19 | let container = persistenceController.persistentContainer
20 | container.acceptShareInvitations(from: [cloudKitShareMetadata], into: sharedStore) { (_, error) in
21 | if let error = error {
22 | print("\(#function): Failed to accept share invitations: \(error)")
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | The scene delegate class.
5 |
6 |
7 | */
8 |
9 | import UIKit
10 | import CloudKit
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 | var window: UIWindow?
14 | /**
15 | To be able to accept a share, add a CKSharingSupported entry in the info.plist file and set it to true.
16 | */
17 | func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
18 | let persistenceController = PersistenceController.shared
19 | let sharedStore = persistenceController.sharedPersistentStore
20 | let container = persistenceController.persistentContainer
21 | container.acceptShareInvitations(from: [cloudKitShareMetadata], into: sharedStore) { (_, error) in
22 | if let error = error {
23 | print("\(#function): Failed to accept share invitations: \(error)")
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Persistence/PersistenceController+Rating.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | An extension that wraps the methods related to the Rating entity.
5 |
6 |
7 | */
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | // MARK: - Convenient methods for managing tags.
13 | //
14 | extension PersistenceController {
15 |
16 | func addRating(value: Int16, relateTo photo: Photo) {
17 | if let context = photo.managedObjectContext {
18 | context.performAndWait {
19 | let rating = Rating(context: context)
20 | rating.value = value
21 | rating.photo = photo
22 | context.save(with: .addRating)
23 | }
24 | }
25 | }
26 |
27 | func deleteRating(_ rating: Rating) {
28 | if let context = rating.managedObjectContext {
29 | context.performAndWait {
30 | context.delete(rating)
31 | context.save(with: .deleteRating)
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/CoreDataCloudKitShareApp.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | The SwiftUI app for iOS.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 |
12 | @main
13 | struct CoreDataCloudKitShareApp: App {
14 | // swiftlint:disable weak_delegate
15 | @UIApplicationDelegateAdaptor var appDelegate: AppDelegate
16 | // swiftlint:enable weak_delegate
17 | private let persistentContainer = PersistenceController.shared.persistentContainer
18 |
19 | var body: some Scene {
20 | #if InitializeCloudKitSchema
21 | WindowGroup {
22 | Text("Initializing CloudKit Schema...").font(.title)
23 | Text("Stop after Xcode says 'no more requests to execute', " +
24 | "then check with CloudKit Console if the schema is created correctly.").padding()
25 | }
26 | #else
27 | WindowGroup {
28 | PhotoGridView()
29 | .environment(\.managedObjectContext, persistentContainer.viewContext)
30 | }
31 | #endif
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 ziqiaochen
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 |
--------------------------------------------------------------------------------
/SwiftUI/FullImageView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that shows a scrollable full size image.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 |
12 | struct FullImageView: View {
13 | @Binding var isPresented: ActiveCover?
14 | var photo: Photo
15 |
16 | private var photoImage: UIImage? {
17 | let photoData = photo.photoData?.data
18 | return photoData != nil ? UIImage(data: photoData!) : nil
19 | }
20 |
21 | var body: some View {
22 | NavigationView {
23 | VStack {
24 | if let image = photoImage {
25 | ScrollView([.horizontal, .vertical]) {
26 | Image(uiImage: image)
27 | }
28 | } else {
29 | Text("The full size image is probably not downloaded from CloudKit.").padding()
30 | Spacer()
31 | }
32 | }
33 | .toolbar {
34 | ToolbarItem(placement: .automatic) {
35 | Button("Dismiss", action: { isPresented = nil })
36 | }
37 | }
38 | .listStyle(PlainListStyle())
39 | .navigationTitle("Full Size Photo")
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets" : [
3 | {
4 | "filename" : "Circular.imageset",
5 | "idiom" : "watch",
6 | "role" : "circular"
7 | },
8 | {
9 | "filename" : "Extra Large.imageset",
10 | "idiom" : "watch",
11 | "role" : "extra-large"
12 | },
13 | {
14 | "filename" : "Graphic Bezel.imageset",
15 | "idiom" : "watch",
16 | "role" : "graphic-bezel"
17 | },
18 | {
19 | "filename" : "Graphic Circular.imageset",
20 | "idiom" : "watch",
21 | "role" : "graphic-circular"
22 | },
23 | {
24 | "filename" : "Graphic Corner.imageset",
25 | "idiom" : "watch",
26 | "role" : "graphic-corner"
27 | },
28 | {
29 | "filename" : "Graphic Extra Large.imageset",
30 | "idiom" : "watch",
31 | "role" : "graphic-extra-large"
32 | },
33 | {
34 | "filename" : "Graphic Large Rectangular.imageset",
35 | "idiom" : "watch",
36 | "role" : "graphic-large-rectangular"
37 | },
38 | {
39 | "filename" : "Modular.imageset",
40 | "idiom" : "watch",
41 | "role" : "modular"
42 | },
43 | {
44 | "filename" : "Utilitarian.imageset",
45 | "idiom" : "watch",
46 | "role" : "utilitarian"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/SwiftUI/AddToExistingShareView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that adds a photo to an existing share.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 | import CloudKit
12 |
13 | struct AddToExistingShareView: View {
14 | @Binding var isPresented: ActiveSheet?
15 | var photo: Photo
16 |
17 | @State private var toggleProgress: Bool = false
18 | @State private var selection: String?
19 |
20 | var body: some View {
21 | ZStack {
22 | SharePickerView(isPresented: $isPresented, selection: $selection) {
23 | Button("Add", action: {
24 | sharePhoto(photo, shareTitle: selection)
25 | })
26 | .disabled(selection == nil)
27 | }
28 | if toggleProgress {
29 | ProgressView()
30 | }
31 | }
32 | }
33 |
34 | private func sharePhoto(_ unsharedPhoto: Photo, shareTitle: String?) {
35 | toggleProgress.toggle()
36 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
37 | let persistenceController = PersistenceController.shared
38 | if let shareTitle = shareTitle, let share = persistenceController.share(with: shareTitle) {
39 | persistenceController.shareObject(unsharedPhoto, to: share)
40 | }
41 | toggleProgress.toggle()
42 | isPresented = nil
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/SwiftUI/PhotoGridItemView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that manages a grid item.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 |
12 | struct PhotoGridItemView: View {
13 | /**
14 | This sample doesn't use editButton and editMode because they are unavalable on watchOS.
15 | It uses the delete button in the action list to handle the deletion.
16 | */
17 | @ObservedObject var photo: Photo
18 | var itemSize: CGSize
19 | private let persistenceController = PersistenceController.shared
20 |
21 | var body: some View {
22 | ZStack(alignment: .topTrailing) {
23 | /**
24 | Show the thumbnail image, or a place holder if the thumbnail data doesn't exist.
25 | */
26 | if let data = photo.thumbnail?.data, let thumbnail = UIImage(data: data) {
27 | Image(uiImage: thumbnail)
28 | .resizable()
29 | .aspectRatio(contentMode: .fit)
30 | .frame(width: itemSize.width, height: itemSize.height)
31 | } else {
32 | Image(systemName: "questionmark.square.dashed")
33 | .font(.system(size: 30))
34 | .frame(width: itemSize.width, height: itemSize.height)
35 | }
36 | topLeftButton()
37 | }
38 | .frame(width: itemSize.width, height: itemSize.height)
39 | .background(Color.gridItemBackground)
40 | }
41 |
42 | @ViewBuilder
43 | private func topLeftButton() -> some View {
44 | if persistenceController.sharedPersistentStore.contains(manageObject: photo) {
45 | Image(systemName: "person.2.circle")
46 | .foregroundColor(.gray)
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/SwiftUI/ManagingSharesView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that manages existing shares.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 | import CloudKit
12 |
13 | struct ManagingSharesView: View {
14 | @Binding var isPresented: ActiveSheet?
15 | @Binding var nextSheet: ActiveSheet?
16 |
17 | @State private var toggleProgress: Bool = false
18 | @State private var selection: String?
19 |
20 | var body: some View {
21 | ZStack {
22 | SharePickerView(isPresented: $isPresented, selection: $selection) {
23 | if let shareTitle = selection, let share = PersistenceController.shared.share(with: shareTitle) {
24 | actionButtons(for: share)
25 | }
26 | }
27 | if toggleProgress {
28 | ProgressView()
29 | }
30 | }
31 | }
32 |
33 | @ViewBuilder
34 | private func actionButtons(for share: CKShare) -> some View {
35 | let persistentStore = share.persistentStore
36 | let isPrivateStore = (persistentStore == PersistenceController.shared.privatePersistentStore)
37 |
38 | Button(isPrivateStore ? "Manage Participants" : "View Participants", action: {
39 | if let share = PersistenceController.shared.share(with: selection!) {
40 | nextSheet = .participantView(share)
41 | isPresented = nil
42 | }
43 | })
44 | .disabled(selection == nil)
45 |
46 | Button(isPrivateStore ? "Stop Sharing" : "Remove Me", action: {
47 | if let share = PersistenceController.shared.share(with: selection!) {
48 | purgeShare(share, in: persistentStore)
49 | }
50 | })
51 | .disabled(selection == nil)
52 |
53 | #if os(iOS)
54 | Button("Manage With UICloudSharingController", action: {
55 | if let share = PersistenceController.shared.share(with: selection!) {
56 | nextSheet = .cloudSharingSheet(share)
57 | isPresented = nil
58 | }
59 | })
60 | .disabled(selection == nil)
61 | #endif
62 | }
63 |
64 | private func purgeShare(_ share: CKShare, in persistentStore: NSPersistentStore?) {
65 | toggleProgress.toggle()
66 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
67 | PersistenceController.shared.purgeObjectsAndRecords(with: share, in: persistentStore)
68 | toggleProgress.toggle()
69 | isPresented = nil
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Persistence/PersistenceController+Photo.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | An extension that wraps the methods related to the Photo entity.
5 |
6 |
7 | */
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | // MARK: - Convenient methods for managing photos.
13 | //
14 | extension PersistenceController {
15 | func addPhoto(photoData: Data, thumbnailData: Data, tagNames: [String] = [], context: NSManagedObjectContext) {
16 | context.perform {
17 | let photo = Photo(context: context)
18 | photo.uniqueName = UUID().uuidString
19 |
20 | let thumbnail = Thumbnail(context: context)
21 | thumbnail.data = thumbnailData
22 | thumbnail.photo = photo
23 |
24 | let photoDataObject = PhotoData(context: context)
25 | photoDataObject.data = photoData
26 | photoDataObject.photo = photo
27 |
28 | for tagName in tagNames {
29 | let existingTag = Tag.tagIfExists(with: tagName, context: context)
30 | let tag = existingTag ?? Tag(context: context)
31 | tag.name = tagName
32 | tag.addToPhotos(photo)
33 | }
34 |
35 | context.save(with: .addPhoto)
36 | }
37 | }
38 |
39 | func delete(photo: Photo) {
40 | if let context = photo.managedObjectContext {
41 | context.perform {
42 | context.delete(photo)
43 | context.save(with: .deletePhoto)
44 | }
45 | }
46 | }
47 |
48 | func photoTransactions(from notification: Notification) -> [NSPersistentHistoryTransaction] {
49 | var results = [NSPersistentHistoryTransaction]()
50 | if let transactions = notification.userInfo?[UserInfoKey.transactions] as? [NSPersistentHistoryTransaction] {
51 | let photoEntityName = Photo.entity().name
52 | for transaction in transactions where transaction.changes != nil {
53 | for change in transaction.changes! where change.changedObjectID.entity.name == photoEntityName {
54 | results.append(transaction)
55 | break // Jump to the next transaction.
56 | }
57 | }
58 | }
59 | return results
60 | }
61 |
62 | func mergeTransactions(_ transactions: [NSPersistentHistoryTransaction], to context: NSManagedObjectContext) {
63 | context.perform {
64 | for transaction in transactions {
65 | context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShareOnWatch/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "role" : "notificationCenter",
6 | "scale" : "2x",
7 | "size" : "24x24",
8 | "subtype" : "38mm"
9 | },
10 | {
11 | "idiom" : "watch",
12 | "role" : "notificationCenter",
13 | "scale" : "2x",
14 | "size" : "27.5x27.5",
15 | "subtype" : "42mm"
16 | },
17 | {
18 | "idiom" : "watch",
19 | "role" : "companionSettings",
20 | "scale" : "2x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "watch",
25 | "role" : "companionSettings",
26 | "scale" : "3x",
27 | "size" : "29x29"
28 | },
29 | {
30 | "idiom" : "watch",
31 | "role" : "notificationCenter",
32 | "scale" : "2x",
33 | "size" : "33x33",
34 | "subtype" : "45mm"
35 | },
36 | {
37 | "idiom" : "watch",
38 | "role" : "appLauncher",
39 | "scale" : "2x",
40 | "size" : "40x40",
41 | "subtype" : "38mm"
42 | },
43 | {
44 | "idiom" : "watch",
45 | "role" : "appLauncher",
46 | "scale" : "2x",
47 | "size" : "44x44",
48 | "subtype" : "40mm"
49 | },
50 | {
51 | "idiom" : "watch",
52 | "role" : "appLauncher",
53 | "scale" : "2x",
54 | "size" : "46x46",
55 | "subtype" : "41mm"
56 | },
57 | {
58 | "idiom" : "watch",
59 | "role" : "appLauncher",
60 | "scale" : "2x",
61 | "size" : "50x50",
62 | "subtype" : "44mm"
63 | },
64 | {
65 | "idiom" : "watch",
66 | "role" : "appLauncher",
67 | "scale" : "2x",
68 | "size" : "51x51",
69 | "subtype" : "45mm"
70 | },
71 | {
72 | "idiom" : "watch",
73 | "role" : "quickLook",
74 | "scale" : "2x",
75 | "size" : "86x86",
76 | "subtype" : "38mm"
77 | },
78 | {
79 | "idiom" : "watch",
80 | "role" : "quickLook",
81 | "scale" : "2x",
82 | "size" : "98x98",
83 | "subtype" : "42mm"
84 | },
85 | {
86 | "idiom" : "watch",
87 | "role" : "quickLook",
88 | "scale" : "2x",
89 | "size" : "108x108",
90 | "subtype" : "44mm"
91 | },
92 | {
93 | "idiom" : "watch",
94 | "role" : "quickLook",
95 | "scale" : "2x",
96 | "size" : "117x117",
97 | "subtype" : "45mm"
98 | },
99 | {
100 | "idiom" : "watch-marketing",
101 | "scale" : "1x",
102 | "size" : "1024x1024"
103 | }
104 | ],
105 | "info" : {
106 | "author" : "xcode",
107 | "version" : 1
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Persistence/CoreDataHelper.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | Extensions that add convenience methods to Core Data.
5 |
6 |
7 | */
8 |
9 | import CoreData
10 | import CloudKit
11 |
12 | extension NSPersistentStore {
13 | func contains(manageObject: NSManagedObject) -> Bool {
14 | let fetchRequest = NSFetchRequest(entityName: manageObject.entity.name!)
15 | fetchRequest.predicate = NSPredicate(format: "self == %@", manageObject)
16 | fetchRequest.affectedStores = [self]
17 |
18 | if let context = manageObject.managedObjectContext,
19 | let result = try? context.count(for: fetchRequest), result > 0 {
20 | return true
21 | }
22 | return false
23 | }
24 | }
25 |
26 | extension NSManagedObject {
27 | var persistentStore: NSPersistentStore? {
28 | let persistenceController = PersistenceController.shared
29 | if persistenceController.sharedPersistentStore.contains(manageObject: self) {
30 | return persistenceController.sharedPersistentStore
31 | } else if persistenceController.privatePersistentStore.contains(manageObject: self) {
32 | return persistenceController.privatePersistentStore
33 | }
34 | return nil
35 | }
36 | }
37 |
38 | extension NSManagedObjectContext {
39 | /**
40 | Contextual information for handling error that happens when saving a managed object context.
41 | */
42 | enum ContextualInfoForSaving: String {
43 | case addPhoto, deletePhoto
44 | case toggleTagging, deleteTag, addTag
45 | case addRating, deleteRating
46 | case sheetOnDismiss
47 | case deduplicateAndWait
48 | }
49 | /**
50 | Save a context and handle the save error. This sample simply prints the error message. Real apps should
51 | consider comprehensive error handling based on the contextual information.
52 | */
53 | func save(with contextualInfo: ContextualInfoForSaving) {
54 | if hasChanges {
55 | do {
56 | try save()
57 | } catch {
58 | print("\(#function): Failed to save Core Data context for \(contextualInfo.rawValue): \(error)")
59 | }
60 | }
61 | }
62 | }
63 |
64 | /**
65 | A convenience method for creating background contexts that specify the app as their transaction author.
66 | */
67 | extension NSPersistentCloudKitContainer {
68 | func newTaskContext() -> NSManagedObjectContext {
69 | let context = newBackgroundContext()
70 | context.transactionAuthor = TransactionAuthor.app
71 | return context
72 | }
73 |
74 | /**
75 | Fetch and return shares in the persistent stores.
76 | */
77 | func fetchShares(in persistentStores: [NSPersistentStore]) throws -> [CKShare] {
78 | var results = [CKShare]()
79 | for persistentStore in persistentStores {
80 | do {
81 | let shares = try fetchShares(in: persistentStore)
82 | results += shares
83 | } catch let error {
84 | print("Failed to fetch shares in \(persistentStore).")
85 | throw error
86 | }
87 | }
88 | return results
89 | }
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare/PhotoPicker.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A UIViewControllerRepresentable that wraps PHPickerViewController.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import PhotosUI
11 | import CoreData
12 |
13 | struct PhotoPicker: UIViewControllerRepresentable {
14 | @Binding var isPresented: ActiveSheet?
15 | let persistenceController = PersistenceController.shared
16 |
17 | func makeUIViewController(context: Context) -> PHPickerViewController {
18 | let configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
19 | let controller = PHPickerViewController(configuration: configuration)
20 | controller.delegate = context.coordinator
21 | return controller
22 | }
23 |
24 | func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
25 | }
26 |
27 | func makeCoordinator() -> PhotoPickerCoordinator {
28 | PhotoPickerCoordinator(photoPicker: self)
29 | }
30 | }
31 |
32 | /**
33 | The coordinator class that saves the picked image to the Core Data store.
34 | */
35 | class PhotoPickerCoordinator: PHPickerViewControllerDelegate {
36 | private var photoPicker: PhotoPicker
37 |
38 | init(photoPicker: PhotoPicker) {
39 | self.photoPicker = photoPicker
40 | }
41 |
42 | func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
43 | for result in results {
44 | result.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
45 | guard let image = object as? UIImage else {
46 | print("Failed to load UIImage from the picker reuslt.")
47 | return
48 | }
49 | self.saveImage(image)
50 | }
51 | }
52 | // The system doesn’t automatically dismiss the picker so toggle isPresented to do that.
53 | photoPicker.isPresented = nil
54 | }
55 |
56 | private func saveImage(_ image: UIImage) {
57 | guard let imageData = image.jpegData(compressionQuality: 1) else {
58 | print("\(#function): Failed to retrieve JPG data and URL of the picked image.")
59 | return
60 | }
61 | guard let thumbnailData = thumbnail(with: imageData)?.jpegData(compressionQuality: 1) else {
62 | print("\(#function): Failed to create a thumbnail for the picked image.")
63 | return
64 | }
65 | let controller = photoPicker.persistenceController
66 | let taskContext = controller.persistentContainer.newTaskContext()
67 | taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
68 | controller.addPhoto(photoData: imageData, thumbnailData: thumbnailData, context: taskContext)
69 | }
70 |
71 | private func thumbnail(with imageData: Data, pixelSize: Int = 120) -> UIImage? {
72 | let options = [kCGImageSourceCreateThumbnailWithTransform: true,
73 | kCGImageSourceCreateThumbnailFromImageAlways: true,
74 | kCGImageSourceThumbnailMaxPixelSize: pixelSize] as CFDictionary
75 | guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil) else {
76 | return nil
77 | }
78 | let imageReference = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options)!
79 | return UIImage(cgImage: imageReference)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/SwiftUI/SharePickerView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that picks an existing share.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 | import CloudKit
12 |
13 | struct SharePickerView: View {
14 | @Binding private var isPresented: ActiveSheet?
15 | @Binding private var selection: String?
16 |
17 | private let actionView: ActionView
18 | @State private var shareTitles = PersistenceController.shared.shareTitles()
19 |
20 | init(isPresented: Binding, selection: Binding, @ViewBuilder actionView: () -> ActionView) {
21 | _isPresented = isPresented
22 | _selection = selection
23 | self.actionView = actionView()
24 | }
25 |
26 | var body: some View {
27 | NavigationView {
28 | VStack {
29 | if shareTitles.isEmpty {
30 | Text("No share exists. Please create a new share for a photo, then try again.").padding()
31 | Spacer()
32 | } else {
33 | Form {
34 | Section(header: Text("Pick a share")) {
35 | ShareListView(selection: $selection, shareTitles: $shareTitles)
36 | }
37 | Section {
38 | actionView
39 | }
40 | }
41 | }
42 | }
43 | .toolbar {
44 | ToolbarItem(placement: .automatic) {
45 | Button("Dismiss", action: { isPresented = nil })
46 | }
47 | }
48 | .listStyle(PlainListStyle())
49 | .navigationTitle("Shares")
50 | }
51 | .onReceive(NotificationCenter.default.storeDidChangePublisher) { notification in
52 | processStoreChangeNotification(notification)
53 | }
54 | }
55 |
56 | /**
57 | Update the share list if necessary. Ignore the notification in the following cases:
58 | - The notification is not relevant to the private database.
59 | - The notification transaction is not empty. When a share changes, Core Data triggers a store remote change notification with no transaction.
60 | */
61 | private func processStoreChangeNotification(_ notification: Notification) {
62 | guard let storeUUID = notification.userInfo?[UserInfoKey.storeUUID] as? String,
63 | storeUUID == PersistenceController.shared.privatePersistentStore.identifier else {
64 | return
65 | }
66 | guard let transactions = notification.userInfo?[UserInfoKey.transactions] as? [NSPersistentHistoryTransaction],
67 | transactions.isEmpty else {
68 | return
69 | }
70 | shareTitles = PersistenceController.shared.shareTitles()
71 | }
72 |
73 | }
74 |
75 | private struct ShareListView: View {
76 | @Binding var selection: String?
77 | @Binding var shareTitles: [String]
78 |
79 | var body: some View {
80 | List(shareTitles, id: \.self) { shareTitle in
81 | HStack {
82 | Text(shareTitle)
83 | Spacer()
84 | if selection == shareTitle {
85 | Image(systemName: "checkmark")
86 | }
87 | }
88 | .contentShape(Rectangle())
89 | .onTapGesture {
90 | selection = (selection == shareTitle) ? nil : shareTitle
91 | }
92 | }
93 | }
94 | }
95 |
96 |
--------------------------------------------------------------------------------
/Persistence/CoreDataCloudKitShare.xcdatamodeld/CoreDataCloudKitShare.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare.xcodeproj/xcshareddata/xcschemes/InitializeCloudKitSchema.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
66 |
68 |
74 |
75 |
76 |
77 |
79 |
80 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare.xcodeproj/xcshareddata/xcschemes/CoreDataCloudKitShare.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
61 |
62 |
65 |
66 |
67 |
68 |
74 |
76 |
82 |
83 |
84 |
85 |
87 |
88 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/Persistence/PersistenceController+Deduplicate.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | An extension that wraps the methods related to deduplicating tags.
5 |
6 |
7 | */
8 |
9 | import CoreData
10 | import CloudKit
11 |
12 | // MARK: - Deduplicate tags
13 | //
14 | extension PersistenceController {
15 | /**
16 | Deduplicate tags that have a same name and are in the same CloudKit record zone, one tag at a time, on the historyQueue.
17 | All peers should eventually reach the same result with no coordination or communication.
18 | */
19 |
20 | //#-code-listing(deduplicateAndWait)
21 | func deduplicateAndWait(tagObjectIDs: [NSManagedObjectID])
22 | //#-end-code-listing
23 | {
24 | /**
25 | Make any store changes on a background context with the transaction author name of this app.
26 | Use performAndWait to serialize the steps. historyQueue runs in the background so this won’t block the main queue.
27 | */
28 | let taskContext = persistentContainer.newTaskContext()
29 | taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
30 | taskContext.performAndWait {
31 | tagObjectIDs.forEach { tagObjectID in
32 | deduplicate(tagObjectID: tagObjectID, performingContext: taskContext)
33 | }
34 | taskContext.save(with: .deduplicateAndWait)
35 | }
36 | }
37 |
38 | /**
39 | Deduplicate one single tag.
40 | */
41 | private func deduplicate(tagObjectID: NSManagedObjectID, performingContext: NSManagedObjectContext) {
42 | /**
43 | tag.name can be nil when the app inserts a tag and then ( before processing the insertion ) delete it.
44 | In that case, silently ignore the deleted tag.
45 | */
46 | guard let tag = performingContext.object(with: tagObjectID) as? Tag,
47 | let tagName = tag.name else {
48 | print("\(#function): Ignore a tag that was deleted: \(tagObjectID)")
49 | return
50 | }
51 | /**
52 | Fetch all tags with the same name, sorted by uuid, and return if there are no duplicates.
53 | */
54 | let fetchRequest: NSFetchRequest = Tag.fetchRequest()
55 | fetchRequest.sortDescriptors = [NSSortDescriptor(key: Tag.Schema.uuid.rawValue, ascending: true)]
56 | fetchRequest.predicate = NSPredicate(format: "\(Tag.Schema.name.rawValue) == %@", tagName)
57 | guard var duplicatedTags = try? performingContext.fetch(fetchRequest), duplicatedTags.count > 1 else {
58 | return
59 | }
60 |
61 | /**
62 | Filter out the tags that are not in the same CloudKit record zone.
63 | Only tags that have the same name and are in the same record zone are duplicates.
64 | The tag zone ID can be nil, which means it isn't a shared tag. The filter rule is still valid in that case.
65 | */
66 | let tagZoneID = persistentContainer.recordID(for: tag.objectID)?.zoneID
67 | duplicatedTags = duplicatedTags.filter {
68 | self.persistentContainer.recordID(for: $0.objectID)?.zoneID == tagZoneID
69 | }
70 |
71 | guard duplicatedTags.count > 1 else {
72 | return
73 | }
74 | /**
75 | Pick the first tag as the winner.
76 | */
77 | print("\(#function): Deduplicating tag with name: \(tagName), count: \(duplicatedTags.count)")
78 | let winner = duplicatedTags.first!
79 | duplicatedTags.removeFirst()
80 | remove(duplicatedTags: duplicatedTags, winner: winner, performingContext: performingContext)
81 | }
82 |
83 | /**
84 | Remove duplicate tags from their respective photos, replacing them with the winner.
85 | */
86 | private func remove(duplicatedTags: [Tag], winner: Tag, performingContext: NSManagedObjectContext) {
87 | duplicatedTags.forEach { tag in
88 | if let photoSet = tag.photos {
89 | for case let photo as Photo in photoSet {
90 | photo.removeFromTags(tag)
91 | photo.addToTags(winner)
92 | }
93 | }
94 | performingContext.delete(tag)
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Persistence/PersistenceController+Tag.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | Extensions that wrap the methods related to the Tag entity.
5 |
6 |
7 | */
8 |
9 | import Foundation
10 | import CoreData
11 | import CloudKit
12 |
13 | // MARK: - Convenient methods for managing tags.
14 | //
15 | extension PersistenceController {
16 | func numberOfTags(with tagName: String) -> Int {
17 | let fetchRequest: NSFetchRequest = Tag.fetchRequest()
18 | fetchRequest.predicate = NSPredicate(format: "\(Tag.Schema.name.rawValue) == %@", tagName)
19 |
20 | let number = try? persistentContainer.viewContext.count(for: fetchRequest)
21 | return number ?? 0
22 | }
23 |
24 | func addTag(name: String, relateTo photo: Photo) {
25 | if let context = photo.managedObjectContext {
26 | context.performAndWait {
27 | let tag = Tag(context: context)
28 | tag.name = name
29 | tag.uuid = UUID()
30 | tag.addToPhotos(photo)
31 | context.save(with: .addTag)
32 | }
33 | }
34 | }
35 |
36 | func deleteTag(_ tag: Tag) {
37 | if let context = tag.managedObjectContext {
38 | context.performAndWait {
39 | context.delete(tag)
40 | context.save(with: .deleteTag)
41 | }
42 | }
43 | }
44 |
45 | func toggleTagging(photo: Photo, tag: Tag) {
46 | if let context = photo.managedObjectContext {
47 | context.performAndWait {
48 | if let photoTags = photo.tags, photoTags.contains(tag) {
49 | photo.removeFromTags(tag)
50 | } else {
51 | photo.addToTags(tag)
52 | }
53 | context.save(with: .toggleTagging)
54 | }
55 | }
56 | }
57 | /**
58 | Return the tags that the app can use to tag the specified photo (or in the same CloudKit zone as the photo).
59 | */
60 | func filterTags(from tags: [Tag], forTagging photo: Photo) -> [Tag] {
61 | guard let context = photo.managedObjectContext else {
62 | print("\(#function): Tagging a photo that isn't in a context is unsupported.")
63 | return []
64 | }
65 | /**
66 | Fetch the share for the photo
67 | */
68 | var photoShare: CKShare?
69 | if let result = try? persistentContainer.fetchShares(matching: [photo.objectID]) {
70 | photoShare = result[photo.objectID]
71 | }
72 | /**
73 | Gather the object IDs of the tags that are valid for tagging the photo.
74 | - Tags that are already in photo.tags are valid.
75 | - Tags that have the same share as photoShare is valid.
76 | */
77 | var filteredTags = [Tag]()
78 | context.performAndWait {
79 | for tag in tags {
80 | if let photoTags = photo.tags, photoTags.contains(tag) {
81 | filteredTags.append(tag)
82 | continue
83 | }
84 | let tagShare = existingShare(tag: tag)
85 | if photoShare?.recordID.zoneID == tagShare?.recordID.zoneID {
86 | filteredTags.append(tag)
87 | }
88 | }
89 | }
90 | return filteredTags
91 | }
92 |
93 | /**
94 | Fetch and return the share of the tag and its related photos.
95 | Consider the related photos as well.
96 | */
97 | private func existingShare(tag: Tag) -> CKShare? {
98 | var objectIDs = [tag.objectID]
99 | if let photoSet = tag.photos, let photos = Array(photoSet) as? [Photo] {
100 | objectIDs += photos.map { $0.objectID }
101 | }
102 | let result = try? persistentContainer.fetchShares(matching: objectIDs)
103 | return result?.values.first
104 | }
105 | }
106 |
107 | // MARK: - An extension for Tag.
108 | //
109 | extension Tag {
110 | /**
111 | The name of relevant tag attributes.
112 | */
113 | enum Schema: String {
114 | case name, uuid
115 | }
116 |
117 | class func tagIfExists(with name: String, context: NSManagedObjectContext) -> Tag? {
118 | let fetchRequest: NSFetchRequest = Tag.fetchRequest()
119 | fetchRequest.predicate = NSPredicate(format: "\(Schema.name.rawValue) == %@", name)
120 | let tags = try? context.fetch(fetchRequest)
121 | return tags?.first
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare.xcodeproj/xcshareddata/xcschemes/CoreDataCloudKitShareOnWatch.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 |
71 |
73 |
79 |
80 |
81 |
82 |
85 |
86 |
89 |
90 |
93 |
94 |
95 |
96 |
102 |
104 |
110 |
111 |
112 |
113 |
115 |
116 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/SwiftUI/RatingView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that manages photo rating.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 |
12 | struct RatingView: View {
13 | @Binding var isPresented: ActiveSheet?
14 |
15 | @State private var toggleProgress: Bool = false
16 | @State private var wasPhotoDeleted = false
17 | private let photo: Photo
18 | private let canUpdate: Bool
19 |
20 | private let fetchRequest: FetchRequest
21 | private var ratings: FetchedResults {
22 | return fetchRequest.wrappedValue
23 | }
24 |
25 | init(isPresented: Binding, photo: Photo) {
26 | _isPresented = isPresented
27 | self.photo = photo
28 |
29 | let nsFetchRequest = Rating.fetchRequest()
30 | nsFetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Rating.value, ascending: true)]
31 | nsFetchRequest.predicate = NSPredicate(format: "photo = %@", photo)
32 | fetchRequest = FetchRequest(fetchRequest: nsFetchRequest, animation: .default)
33 |
34 | let container = PersistenceController.shared.persistentContainer
35 | canUpdate = container.canUpdateRecord(forManagedObjectWith: photo.objectID)
36 | }
37 |
38 | var body: some View {
39 | NavigationView {
40 | VStack {
41 | if wasPhotoDeleted {
42 | Text("The photo for rating was deleted remotely.").padding()
43 | Spacer()
44 | } else {
45 | ratingListView()
46 | }
47 | }
48 | .toolbar {
49 | ToolbarItem(placement: .automatic) {
50 | Button("Dismiss", action: { isPresented = nil })
51 | }
52 | }
53 | .listStyle(PlainListStyle())
54 | .navigationTitle("Ratings")
55 | }
56 | .onReceive(NotificationCenter.default.storeDidChangePublisher) { _ in
57 | wasPhotoDeleted = photo.isDeleted
58 | }
59 | }
60 |
61 | /**
62 | List -> Section header + section content triggers a strange animation when deleting an item.
63 | Moving the header out (like below) fixes the animation issue, but the toolbar item doesn't work on watchOS.
64 | SectionHeader().padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 0))
65 | List {
66 | SectionContent()
67 | }
68 | */
69 | @ViewBuilder
70 | private func ratingListView() -> some View {
71 | ZStack {
72 | List {
73 | Section(header: sectionHeader()) {
74 | sectionContent()
75 | }
76 | }
77 | if toggleProgress {
78 | ProgressView()
79 | }
80 | }
81 | }
82 |
83 | @ViewBuilder
84 | private func sectionHeader() -> some View {
85 | if canUpdate {
86 | RatingListHeader(toggleProgress: $toggleProgress, photo: photo)
87 | }
88 | }
89 |
90 | @ViewBuilder
91 | private func sectionContent() -> some View {
92 | ForEach(ratings, id: \.self) { rating in
93 | HStack {
94 | ForEach(1..<6) { index in
95 | Image(systemName: rating.value >= index ? "star.fill": "star")
96 | .foregroundColor(.gray)
97 | }
98 | }
99 | }
100 | .onDelete(perform: deleteRatings)
101 | }
102 |
103 | private func deleteRatings(offsets: IndexSet) {
104 | if canUpdate {
105 | withAnimation {
106 | let ratingsToBeDeleted = offsets.map { ratings[$0] }
107 | for rating in ratingsToBeDeleted {
108 | PersistenceController.shared.deleteRating(rating)
109 | }
110 | }
111 | }
112 | }
113 | }
114 |
115 | struct RatingListHeader: View {
116 | @Binding var toggleProgress: Bool
117 | let photo: Photo
118 |
119 | @State var ratingValue: Int = 3
120 |
121 | var body: some View {
122 | HStack {
123 | ForEach(1..<6, id: \.self) { index in
124 | Button(action: { ratingValue = index }) {
125 | Image(systemName: ratingValue >= index ? "star.fill": "star")
126 | }
127 | .buttonStyle(.plain)
128 | Spacer().frame(minWidth: 1, idealWidth: 20, maxWidth: 30)
129 | }
130 | Spacer()
131 | Button(action: addRating) {
132 | Image(systemName: "plus.circle")
133 | .imageScale(.large)
134 | .font(.system(size: 18))
135 | }
136 | .buttonStyle(.plain)
137 | }
138 | .frame(height: 30)
139 | .padding(5)
140 | .background(Color.listHeaderBackground)
141 | }
142 | /**
143 | Toggle the progress view.
144 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1): Allow 0.1 second to show the progress view.
145 | */
146 | private func addRating() {
147 | toggleProgress.toggle()
148 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
149 | withAnimation {
150 | PersistenceController.shared.addRating(value: Int16(ratingValue), relateTo: photo)
151 | toggleProgress.toggle()
152 | }
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/SwiftUI/PhotoContextMenu.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that manages the actions on a photo.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 | import CloudKit
12 |
13 | struct PhotoContextMenu: View {
14 | @Binding var activeSheet: ActiveSheet?
15 | @Binding var nextSheet: ActiveSheet?
16 | private let photo: Photo
17 |
18 | @State private var isPhotoShared: Bool
19 | @State private var hasAnyShare: Bool
20 | @State private var toggleProgress: Bool = false
21 |
22 | init(activeSheet: Binding, nextSheet: Binding, photo: Photo) {
23 | _activeSheet = activeSheet
24 | _nextSheet = nextSheet
25 | self.photo = photo
26 | isPhotoShared = (PersistenceController.shared.existingShare(photo: photo) != nil)
27 | hasAnyShare = PersistenceController.shared.shareTitles().isEmpty ? false : true
28 | }
29 |
30 | var body: some View {
31 | /**
32 | CloudKit has a limit on how many zones a database can have. To avoid hitting the limit,
33 | apps use the existing share if possible.
34 | */
35 | ZStack {
36 | ScrollView {
37 | menuButtons()
38 | }
39 | if toggleProgress {
40 | ProgressView()
41 | }
42 | }
43 | .onReceive(NotificationCenter.default.storeDidChangePublisher) { notification in
44 | processStoreChangeNotification(notification)
45 | }
46 | }
47 |
48 | @ViewBuilder
49 | private func menuButtons() -> some View {
50 | /**
51 | For photos in the private database, allow creating a new share or adding to an existing share.
52 | For photos in the shared database, allow managing participation.
53 | */
54 | if PersistenceController.shared.privatePersistentStore.contains(manageObject: photo) {
55 | Button("Create New Share", action: {
56 | createNewShare(photo: photo)
57 | })
58 | .disabled(isPhotoShared)
59 |
60 | Button("Add to Existing Share", action: {
61 | activeSheet = .sharePicker(photo)
62 | })
63 | .disabled(isPhotoShared || !hasAnyShare)
64 | } else {
65 | Button("Manage Participation", action: {
66 | manageParticipation(photo: photo)
67 | })
68 | }
69 | /**
70 | Tagging and rating.
71 | */
72 | Divider()
73 | Button("Tag", action: {
74 | activeSheet = .taggingView(photo)
75 | })
76 | Button("Rate", action: {
77 | activeSheet = .ratingView(photo)
78 | })
79 | /**
80 | Show the delete button if the user is editing photos and has the permission to delete.
81 | */
82 | if PersistenceController.shared.persistentContainer.canDeleteRecord(forManagedObjectWith: photo.objectID) {
83 | Divider()
84 | Button("Delete", role: .destructive, action: {
85 | PersistenceController.shared.delete(photo: photo)
86 | activeSheet = nil
87 | })
88 | }
89 | }
90 |
91 | /**
92 | Use UICloudSharingController to manage the share on iOS.
93 | On watchOS, UICloudSharingController is unavailable, so create the share using Core Data API.
94 | */
95 | #if os(iOS)
96 | private func createNewShare(photo: Photo) {
97 | PersistenceController.shared.presentCloudSharingController(photo: photo)
98 | }
99 |
100 | private func manageParticipation(photo: Photo) {
101 | PersistenceController.shared.presentCloudSharingController(photo: photo)
102 | }
103 |
104 | #elseif os(watchOS)
105 | /**
106 | Sharing a photo can take a while so dispatch to a global queue so SwiftUI has a chance to show the progress view.
107 | @State variables are thread-safe, so don't need to dispatch back the main queue.
108 | */
109 | private func createNewShare(photo: Photo) {
110 | toggleProgress.toggle()
111 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
112 | PersistenceController.shared.shareObject(photo, to: nil) { share, error in
113 | toggleProgress.toggle()
114 | if let share = share {
115 | nextSheet = .participantView(share)
116 | activeSheet = nil
117 | }
118 | }
119 | }
120 | }
121 |
122 | private func manageParticipation(photo: Photo) {
123 | nextSheet = .managingSharesView
124 | activeSheet = nil
125 | }
126 | #endif
127 |
128 | /**
129 | Ignore the notification in the following cases:
130 | - It is not relevant to the private database.
131 | - It doesn't have any transaction. When a share changes, Core Data triggers a store remote change notification with no transaction.
132 | */
133 | private func processStoreChangeNotification(_ notification: Notification) {
134 | guard let storeUUID = notification.userInfo?[UserInfoKey.storeUUID] as? String,
135 | storeUUID == PersistenceController.shared.privatePersistentStore.identifier else {
136 | return
137 | }
138 | guard let transactions = notification.userInfo?[UserInfoKey.transactions] as? [NSPersistentHistoryTransaction],
139 | transactions.isEmpty else {
140 | return
141 | }
142 | isPhotoShared = (PersistenceController.shared.existingShare(photo: photo) != nil)
143 | hasAnyShare = PersistenceController.shared.shareTitles().isEmpty ? false : true
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/Persistence/PersistenceController+History.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | Extensions that wraps the methods related to persistence history processing.
5 |
6 |
7 | */
8 |
9 | import CoreData
10 | import CloudKit
11 |
12 | // MARK: - Notification handlers that trigger history processing.
13 | //
14 | extension PersistenceController {
15 | /**
16 | Handle .NSPersistentStoreRemoteChange notifications.
17 | Process persistent history to merge relevant changes to the context, and deduplicate the tags if necessary.
18 | */
19 | @objc
20 | func storeRemoteChange(_ notification: Notification) {
21 | guard let storeUUID = notification.userInfo?[NSStoreUUIDKey] as? String,
22 | [privatePersistentStore.identifier, sharedPersistentStore.identifier].contains(storeUUID) else {
23 | print("\(#function): Ignore a store remote Change notification because of no valid storeUUID.")
24 | return
25 | }
26 | processHistoryAsynchronously(storeUUID: storeUUID)
27 | }
28 |
29 | /**
30 | Handle the container's event changed notifications (NSPersistentCloudKitContainer.eventChangedNotification).
31 | */
32 | @objc
33 | func containerEventChanged(_ notification: Notification) {
34 | guard let value = notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey],
35 | let event = value as? NSPersistentCloudKitContainer.Event else {
36 | print("\(#function): Failed to retrieve the container event from notification.userInfo.")
37 | return
38 | }
39 | if event.error != nil {
40 | print("\(#function): Received a persistent CloudKit container event changed notification.\n\(event)")
41 | }
42 | }
43 | }
44 |
45 | // MARK: - Process persistent historty asynchronously
46 | //
47 | extension PersistenceController {
48 | /**
49 | Process persistent history, posting any relevant transactions to the current view.
50 | This method processes the new history since the last history token, and is simply a fetch if there is no new history.
51 | */
52 | private func processHistoryAsynchronously(storeUUID: String) {
53 | historyQueue.addOperation {
54 | let taskContext = self.persistentContainer.newTaskContext()
55 | taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
56 | taskContext.performAndWait {
57 | self.performHistoryProcessing(storeUUID: storeUUID, performingContext: taskContext)
58 | }
59 | }
60 | }
61 |
62 | private func performHistoryProcessing(storeUUID: String, performingContext: NSManagedObjectContext) {
63 | /**
64 | Fetch history received from outside the app since the last timestamp
65 | */
66 | //#-code-listing(fetchHistory)
67 | let lastHistoryToken = historyToken(with: storeUUID)
68 | let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
69 | let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
70 | historyFetchRequest.predicate = NSPredicate(format: "author != %@", TransactionAuthor.app)
71 | request.fetchRequest = historyFetchRequest
72 |
73 | if privatePersistentStore.identifier == storeUUID {
74 | request.affectedStores = [privatePersistentStore]
75 | } else if sharedPersistentStore.identifier == storeUUID {
76 | request.affectedStores = [sharedPersistentStore]
77 | }
78 | //#-end-code-listing
79 |
80 | let result = (try? performingContext.execute(request)) as? NSPersistentHistoryResult
81 | guard let transactions = result?.result as? [NSPersistentHistoryTransaction] else {
82 | return
83 | }
84 | // print("\(#function): Processing transactions: \(transactions.count).")
85 |
86 | /**
87 | Post transactions so observers can update UI if necessary, even when transactions is empty,
88 | because when a share changes, Core Data triggers a store remote change notification with no transaction.
89 | */
90 | let userInfo: [String: Any] = [UserInfoKey.storeUUID: storeUUID, UserInfoKey.transactions: transactions]
91 | NotificationCenter.default.post(name: .cdcksStoreDidChange, object: self, userInfo: userInfo)
92 | /**
93 | Update the history token using the last transaction. The last transaction has the latest token.
94 | */
95 | if let newToken = transactions.last?.token {
96 | updateHistoryToken(with: storeUUID, newToken: newToken)
97 | }
98 |
99 | /**
100 | Limit to the private store so only owners can deduplicate the tags. Owners have full access to the private database, and so
101 | don't need to worry about the permissions.
102 | */
103 | guard !transactions.isEmpty, storeUUID == privatePersistentStore.identifier else {
104 | return
105 | }
106 | /**
107 | Deduplicate the new tags.
108 | Only tags that are not shared or have the same share are deduplicated.
109 | */
110 | var newTagObjectIDs = [NSManagedObjectID]()
111 | let tagEntityName = Tag.entity().name
112 |
113 | for transaction in transactions where transaction.changes != nil {
114 | for change in transaction.changes! {
115 | if change.changedObjectID.entity.name == tagEntityName && change.changeType == .insert {
116 | newTagObjectIDs.append(change.changedObjectID)
117 | }
118 | }
119 | }
120 | if !newTagObjectIDs.isEmpty {
121 | deduplicateAndWait(tagObjectIDs: newTagObjectIDs)
122 | }
123 | }
124 |
125 | /**
126 | Track the last history tokens for the stores.
127 | The historyQueue reads the token when executing operations, and updates it after completing the processing.
128 | Access this user default from the history queue.
129 | */
130 | private func historyToken(with storeUUID: String) -> NSPersistentHistoryToken? {
131 | let key = "HistoryToken" + storeUUID
132 | if let data = UserDefaults.standard.data(forKey: key) {
133 | return try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: data)
134 | }
135 | return nil
136 | }
137 |
138 | private func updateHistoryToken(with storeUUID: String, newToken: NSPersistentHistoryToken) {
139 | let key = "HistoryToken" + storeUUID
140 | let data = try? NSKeyedArchiver.archivedData(withRootObject: newToken, requiringSecureCoding: true)
141 | UserDefaults.standard.set(data, forKey: key)
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/SwiftUI/TaggingView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that manages photo tagging.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 |
12 | struct TaggingView: View {
13 | @Binding var isPresented: ActiveSheet?
14 |
15 | @State private var filterTagName = ""
16 | @State private var wasPhotoDeleted: Bool
17 |
18 | private let photo: Photo
19 | /**
20 | Retrieving the photo's persistent store (photo.persistentStore) is expensive, so cache it with a member varible
21 | and provide it to FilteredTagList, as FilteredTagList refreshes frequently when the user inputs.
22 | */
23 | private let affectedStore: NSPersistentStore?
24 |
25 | init(isPresented: Binding, photo: Photo) {
26 | _isPresented = isPresented
27 | self.photo = photo
28 | wasPhotoDeleted = photo.isDeleted
29 | affectedStore = photo.persistentStore
30 | }
31 |
32 | var body: some View {
33 | NavigationView {
34 | VStack {
35 | if wasPhotoDeleted {
36 | Text("The photo was deleted remotely.").padding()
37 | Spacer()
38 | } else {
39 | FilteredTagList(filterTagName: $filterTagName, photo: photo, affectedStore: affectedStore)
40 | }
41 | }
42 | .toolbar {
43 | ToolbarItem(placement: .automatic) {
44 | Button("Dismiss", action: { isPresented = nil })
45 | }
46 | }
47 | .listStyle(PlainListStyle())
48 | .navigationTitle("Tags")
49 | }
50 | .onReceive(NotificationCenter.default.storeDidChangePublisher) { _ in
51 | wasPhotoDeleted = photo.isDeleted
52 | }
53 | }
54 | }
55 |
56 | struct FilteredTagList: View {
57 | @Environment(\.managedObjectContext) private var viewContext
58 | @Binding var filterTagName: String
59 |
60 | private let photo: Photo
61 | private let canUpdate: Bool
62 | private let affectedStore: NSPersistentStore?
63 |
64 | @State private var toggleProgress: Bool = false
65 |
66 | private let fetchRequest: FetchRequest
67 | private var tags: [Tag] {
68 | let allTags = Array(fetchRequest.wrappedValue)
69 | return PersistenceController.shared.filterTags(from: allTags, forTagging: photo)
70 | }
71 |
72 | /**
73 | Retrieving the photo's persistent store (photo.persistentStore) is expensive, so relies on the parent view to provide it.
74 | */
75 | init(filterTagName: Binding, photo: Photo, affectedStore: NSPersistentStore?) {
76 | _filterTagName = filterTagName
77 | self.photo = photo
78 | self.affectedStore = affectedStore
79 | /**
80 | Use a fetch request with a predicate based on the specified filtered tag name, and specify its affected store.
81 | */
82 | var predicate = NSPredicate(value: true)
83 | if !filterTagName.wrappedValue.isEmpty {
84 | predicate = NSPredicate(format: "name CONTAINS[cd] %@", filterTagName.wrappedValue)
85 | }
86 | let nsFetchRequest = Tag.fetchRequest()
87 | nsFetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Tag.name, ascending: true)]
88 | nsFetchRequest.predicate = predicate
89 | if let affectedStore = affectedStore {
90 | nsFetchRequest.affectedStores = [affectedStore]
91 | }
92 |
93 | fetchRequest = FetchRequest(fetchRequest: nsFetchRequest, animation: .default)
94 |
95 | let container = PersistenceController.shared.persistentContainer
96 | canUpdate = container.canUpdateRecord(forManagedObjectWith: photo.objectID)
97 | }
98 |
99 | var body: some View {
100 | ZStack {
101 | /**
102 | List -> Section header + section content triggers a strange animation when deleting an item.
103 | Moving the header out (like below) fixes the animation issue, but the toolbar item doesn't work on watchOS.
104 | SectionHeader().padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 0))
105 | List {
106 | SectionContent()
107 | }
108 | */
109 | List {
110 | Section(header: sectionHeader()) {
111 | sectionContent()
112 | }
113 | }
114 | if toggleProgress {
115 | ProgressView()
116 | }
117 | }
118 | }
119 |
120 | @ViewBuilder
121 | private func sectionHeader() -> some View {
122 | if canUpdate {
123 | TagListHeader(toggleProgress: $toggleProgress, filterTagName: $filterTagName, tags: tags, photo: photo)
124 | }
125 | }
126 |
127 | @ViewBuilder
128 | private func sectionContent() -> some View {
129 | ForEach(tags) { tag in
130 | HStack {
131 | Text("\(tag.name!)")
132 | Spacer()
133 | if let photoTags = photo.tags, photoTags.contains(tag) {
134 | Image(systemName: "checkmark")
135 | }
136 | }
137 | .contentShape(Rectangle())
138 | .onTapGesture { toggleTagging(tag: tag) }
139 | }
140 | .onDelete(perform: deleteTags)
141 | }
142 |
143 | private func deleteTags(offsets: IndexSet) {
144 | if canUpdate {
145 | withAnimation {
146 | let tagsToBeDeleted = offsets.map { tags[$0] }
147 | for tag in tagsToBeDeleted {
148 | PersistenceController.shared.deleteTag(tag)
149 | }
150 | }
151 | }
152 | }
153 |
154 | private func toggleTagging(tag: Tag) {
155 | if canUpdate {
156 | PersistenceController.shared.toggleTagging(photo: photo, tag: tag)
157 | }
158 | }
159 | }
160 |
161 | struct TagListHeader: View {
162 | @Environment(\.managedObjectContext) private var viewContext
163 | @Binding var toggleProgress: Bool
164 | @Binding var filterTagName: String
165 |
166 | private let photo: Photo
167 | private let tags: [Tag]
168 |
169 | init(toggleProgress: Binding, filterTagName: Binding, tags: [Tag], photo: Photo) {
170 | _toggleProgress = toggleProgress
171 | _filterTagName = filterTagName
172 | self.tags = tags
173 | self.photo = photo
174 | }
175 |
176 | var body: some View {
177 | HStack {
178 | TextField( "Name", text: $filterTagName)
179 |
180 | Button(action: addTag) {
181 | Image(systemName: "plus.circle")
182 | .imageScale(.large)
183 | .font(.system(size: 18))
184 | }
185 | .frame(width: 20)
186 | .buttonStyle(.plain)
187 | .disabled(filterTagName.isEmpty || tags.map { $0.name }.contains(filterTagName))
188 | }
189 | .frame(height: 30)
190 | .padding(5)
191 | .background(Color.listHeaderBackground)
192 | }
193 |
194 | private func addTag() {
195 | guard !filterTagName.isEmpty else {
196 | return
197 | }
198 | toggleProgress.toggle()
199 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
200 | withAnimation {
201 | PersistenceController.shared.addTag(name: filterTagName, relateTo: photo)
202 | toggleProgress.toggle()
203 | filterTagName = ""
204 | }
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/SwiftUI/PhotoGridView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that manages a photo collection.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 | import CloudKit
12 |
13 | enum ActiveSheet: Identifiable, Equatable {
14 | #if os(iOS)
15 | case photoPicker // Unavailable on watchOS
16 | #elseif os(watchOS)
17 | case photoContextMenu(Photo) // .contextMenu is deprecated on watchOS so use action list instead.
18 | #endif
19 | case cloudSharingSheet(CKShare)
20 | case managingSharesView
21 | case sharePicker(Photo)
22 | case taggingView(Photo)
23 | case ratingView(Photo)
24 | case participantView(CKShare)
25 | /**
26 | Use the enum member name string as the id for Identifiable.
27 | In the case where an enum has an associated value, use the label, which is equal to the member name string.
28 | */
29 | var id: String {
30 | let mirror = Mirror(reflecting: self)
31 | if let label = mirror.children.first?.label {
32 | return label
33 | } else {
34 | return "\(self)"
35 | }
36 | }
37 | }
38 |
39 | enum ActiveCover: Identifiable, Equatable {
40 | case fullImageView(Photo)
41 | /**
42 | Use the enum member name string as the id for Identifiable.
43 | In the case where an enum has an associated value, use the label, which is equal to the member name string.
44 | */
45 | var id: String {
46 | let mirror = Mirror(reflecting: self)
47 | if let label = mirror.children.first?.label {
48 | return label
49 | } else {
50 | return "\(self)"
51 | }
52 | }
53 | }
54 |
55 | struct PhotoGridView: View {
56 | @Environment(\.managedObjectContext) private var viewContext
57 | @FetchRequest(sortDescriptors: [SortDescriptor(\.uniqueName)],
58 | animation: .default
59 | ) private var photos: FetchedResults
60 |
61 | @State private var activeSheet: ActiveSheet?
62 | @State private var activeCover: ActiveCover?
63 |
64 | /**
65 | The next active sheet to present after dismissing the current sheet.
66 | ManagingSharesView uses this variable to switch to UICloudSharingController or participant view.
67 | */
68 | @State private var nextSheet: ActiveSheet?
69 |
70 | private let persistenceController = PersistenceController.shared
71 | private let kGridCellSize = CGSize(width: 118, height: 118)
72 |
73 | var body: some View {
74 | NavigationView {
75 | VStack {
76 | ScrollView {
77 | if photos.isEmpty {
78 | Text("Tap the add (+) button on the iOS app to add a photo.").padding()
79 | Spacer()
80 | } else {
81 | LazyVGrid(columns: [GridItem(.adaptive(minimum: kGridCellSize.width))]) {
82 | ForEach(photos, id: \.self) { photo in
83 | gridItemView(photo: photo, itemSize: kGridCellSize)
84 | }
85 | }
86 | }
87 | }
88 | }
89 | .toolbar { toolbarItems() }
90 | .navigationTitle("Photos")
91 | .sheet(item: $activeSheet, onDismiss: sheetOnDismiss) { item in
92 | sheetView(with: item)
93 | }
94 | .fullScreenCover(item: $activeCover) { item in
95 | coverView(with: item)
96 | }
97 |
98 | }
99 | .navigationViewStyle(.stack)
100 | .onReceive(NotificationCenter.default.storeDidChangePublisher) { notification in
101 | processStoreChangeNotification(notification)
102 | }
103 | }
104 |
105 | @ViewBuilder
106 | private func gridItemView(photo: Photo, itemSize: CGSize) -> some View {
107 | #if os(iOS)
108 | PhotoGridItemView(photo: photo, itemSize: kGridCellSize)
109 | .contextMenu {
110 | PhotoContextMenu(activeSheet: $activeSheet, nextSheet: $nextSheet, photo: photo)
111 | }
112 | .onTapGesture {
113 | activeCover = .fullImageView(photo)
114 | }
115 | #elseif os(watchOS)
116 | PhotoGridItemView(photo: photo, itemSize: kGridCellSize)
117 | .onTapGesture {
118 | activeSheet = .photoContextMenu(photo)
119 | }
120 | #endif
121 | }
122 |
123 | @ToolbarContentBuilder
124 | private func toolbarItems() -> some ToolbarContent {
125 | #if os(iOS)
126 | ToolbarItem(placement: .navigationBarTrailing) {
127 | Button(action: { activeSheet = .photoPicker }) {
128 | Label("Add Item", systemImage: "plus").labelStyle(.iconOnly)
129 | }
130 | }
131 | ToolbarItem(placement: .bottomBar) {
132 | Button("Manage Shares", action: {
133 | activeSheet = .managingSharesView
134 | })
135 | }
136 | #elseif os(watchOS)
137 | ToolbarItem(placement: .automatic) {
138 | Button("Manage Shares", action: { activeSheet = .managingSharesView })
139 | }
140 | #endif
141 | }
142 |
143 | @ViewBuilder
144 | private func sheetView(with item: ActiveSheet) -> some View {
145 | switch item {
146 | #if os(iOS)
147 | case .photoPicker:
148 | PhotoPicker(isPresented: $activeSheet)
149 | #elseif os(watchOS)
150 | case .photoContextMenu(let photo):
151 | PhotoContextMenu(activeSheet: $activeSheet, nextSheet: $nextSheet, photo: photo)
152 | #endif
153 |
154 | case .cloudSharingSheet(_):
155 | // CloudSharingSheet(isPresented: $activeSheet, share: share) // Not used due to Rdar://83684057.
156 | EmptyView()
157 | case .managingSharesView:
158 | ManagingSharesView(isPresented: $activeSheet, nextSheet: $nextSheet)
159 |
160 | case .sharePicker(let photo):
161 | AddToExistingShareView(isPresented: $activeSheet, photo: photo)
162 |
163 | case .taggingView(let photo):
164 | TaggingView(isPresented: $activeSheet, photo: photo)
165 |
166 | case .ratingView(let photo):
167 | RatingView(isPresented: $activeSheet, photo: photo)
168 |
169 | case .participantView(let share):
170 | ParticipantView(isPresented: $activeSheet, share: share)
171 | }
172 | }
173 |
174 | /**
175 | Present the next active sheet if necessary.
176 | Dispatch asynchronously to the next run loop so the presentation occurs after the current sheet's dismissal.
177 | */
178 | private func sheetOnDismiss() {
179 | guard let nextActiveSheet = nextSheet else {
180 | return
181 | }
182 | switch nextActiveSheet {
183 | case .cloudSharingSheet(let share):
184 | DispatchQueue.main.async {
185 | persistenceController.presentCloudSharingController(share: share)
186 | }
187 | default:
188 | DispatchQueue.main.async {
189 | activeSheet = nextActiveSheet
190 | }
191 | }
192 | nextSheet = nil
193 | }
194 |
195 | @ViewBuilder
196 | private func coverView(with item: ActiveCover) -> some View {
197 | switch item {
198 | case .fullImageView(let photo):
199 | FullImageView(isPresented: $activeCover, photo: photo)
200 | }
201 | }
202 |
203 | /**
204 | Merge the transactions if any.
205 | */
206 | private func processStoreChangeNotification(_ notification: Notification) {
207 | let transactions = persistenceController.photoTransactions(from: notification)
208 | if !transactions.isEmpty {
209 | persistenceController.mergeTransactions(transactions, to: viewContext)
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/Persistence/PersistenceController.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A class that sets up the Core Data stack.
5 |
6 |
7 | */
8 |
9 | import Foundation
10 | import CoreData
11 | import CloudKit
12 | import SwiftUI
13 |
14 | let gCloudKitContainerIdentifier = "iCloud.com.example.ziqiao-samplecode.CoreDataCloudKitShare"
15 |
16 | /**
17 | This app doesn't necessarily post notifications from the main queue.
18 | */
19 | extension Notification.Name {
20 | static let cdcksStoreDidChange = Notification.Name("cdcksStoreDidChange")
21 | }
22 |
23 | struct UserInfoKey {
24 | static let storeUUID = "storeUUID"
25 | static let transactions = "transactions"
26 | }
27 |
28 | struct TransactionAuthor {
29 | static let app = "app"
30 | }
31 |
32 | class PersistenceController: NSObject, ObservableObject {
33 | static let shared = PersistenceController()
34 |
35 | lazy var persistentContainer: NSPersistentCloudKitContainer = {
36 | /**
37 | Prepare the parent folder for the Core Data stores.
38 | A Core Data store has companion files, so it is a good practice to put a store under a folder.
39 | */
40 | let baseURL = NSPersistentContainer.defaultDirectoryURL()
41 | let storeFolderURL = baseURL.appendingPathComponent("CoreDataStores")
42 | let privateStoreFolderURL = storeFolderURL.appendingPathComponent("Private")
43 | let sharedStoreFolderURL = storeFolderURL.appendingPathComponent("Shared")
44 |
45 | let fileManager = FileManager.default
46 | for folderURL in [privateStoreFolderURL, sharedStoreFolderURL] where !fileManager.fileExists(atPath: folderURL.path) {
47 | do {
48 | try fileManager.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil)
49 | } catch {
50 | fatalError("#\(#function): Failed to create the store folder: \(error)")
51 | }
52 | }
53 |
54 | let container = NSPersistentCloudKitContainer(name: "CoreDataCloudKitShare")
55 |
56 | /**
57 | Grab the default (first) store and associate it with the CloudKit private database.
58 | Set up the store description by:
59 | - Specifying a file name for the store.
60 | - Enabling history tracking and remote notifications.
61 | - Specifying the iCloud container and database scope.
62 | */
63 | guard let privateStoreDescription = container.persistentStoreDescriptions.first else {
64 | fatalError("#\(#function): Failed to retrieve a persistent store description.")
65 | }
66 | privateStoreDescription.url = privateStoreFolderURL.appendingPathComponent("private.sqlite")
67 |
68 | //#-code-listing(setOption)
69 | privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
70 | privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
71 | //#-end-code-listing
72 |
73 | //#-code-listing(NSPersistentCloudKitContainerOptions)
74 | let cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: gCloudKitContainerIdentifier)
75 | //#-end-code-listing
76 |
77 | cloudKitContainerOptions.databaseScope = .private
78 | privateStoreDescription.cloudKitContainerOptions = cloudKitContainerOptions
79 |
80 | /**
81 | Similarly, add a second store and associate it with the CloudKit shared database.
82 | */
83 | guard let sharedStoreDescription = privateStoreDescription.copy() as? NSPersistentStoreDescription else {
84 | fatalError("#\(#function): Copying the private store description returned an unexpected value.")
85 | }
86 | sharedStoreDescription.url = sharedStoreFolderURL.appendingPathComponent("shared.sqlite")
87 |
88 | let sharedStoreOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: gCloudKitContainerIdentifier)
89 | sharedStoreOptions.databaseScope = .shared
90 | sharedStoreDescription.cloudKitContainerOptions = sharedStoreOptions
91 |
92 | /**
93 | Load the persistent stores
94 | */
95 | container.persistentStoreDescriptions.append(sharedStoreDescription)
96 | container.loadPersistentStores(completionHandler: { (loadedStoreDescription, error) in
97 | guard error == nil else {
98 | fatalError("#\(#function): Failed to load persistent stores:\(error!)")
99 | }
100 | guard let cloudKitContainerOptions = loadedStoreDescription.cloudKitContainerOptions else {
101 | return
102 | }
103 | if cloudKitContainerOptions.databaseScope == .private {
104 | self._privatePersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
105 | } else if cloudKitContainerOptions.databaseScope == .shared {
106 | self._sharedPersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
107 | }
108 | })
109 |
110 | /**
111 | Run initializeCloudKitSchema() once to update the CloudKit schema every time you change the Core Data model.
112 | Do not call this code in the production environment.
113 | */
114 | #if InitializeCloudKitSchema
115 | do {
116 | try container.initializeCloudKitSchema()
117 | } catch {
118 | print("\(#function): initializeCloudKitSchema: \(error)")
119 | }
120 | #else
121 | container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
122 | container.viewContext.transactionAuthor = TransactionAuthor.app
123 |
124 | /**
125 | Automatically merge the changes from other contexts.
126 | */
127 | container.viewContext.automaticallyMergesChangesFromParent = true
128 |
129 | /**
130 | Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
131 | */
132 | do {
133 | try container.viewContext.setQueryGenerationFrom(.current)
134 | } catch {
135 | fatalError("#\(#function): Failed to pin viewContext to the current generation:\(error)")
136 | }
137 |
138 | /**
139 | Observe the following notifications:
140 | - The remote change notifications from container.persistentStoreCoordinator
141 | - The .NSManagedObjectContextDidSave notifications from any context.
142 | - The event change notifications from the container.
143 | */
144 | NotificationCenter.default.addObserver(self, selector: #selector(storeRemoteChange(_:)),
145 | name: .NSPersistentStoreRemoteChange,
146 | object: container.persistentStoreCoordinator)
147 | NotificationCenter.default.addObserver(self, selector: #selector(containerEventChanged(_:)),
148 | name: NSPersistentCloudKitContainer.eventChangedNotification,
149 | object: container)
150 | #endif
151 | return container
152 | }()
153 |
154 | private var _privatePersistentStore: NSPersistentStore?
155 | var privatePersistentStore: NSPersistentStore {
156 | return _privatePersistentStore!
157 | }
158 |
159 | private var _sharedPersistentStore: NSPersistentStore?
160 | var sharedPersistentStore: NSPersistentStore {
161 | return _sharedPersistentStore!
162 | }
163 |
164 | lazy var cloudKitContainer: CKContainer = {
165 | return CKContainer(identifier: gCloudKitContainerIdentifier)
166 | }()
167 |
168 | /**
169 | An operation queue for handling history processing tasks: watching changes, deduplicating tags, and triggering UI updates if needed.
170 | */
171 | lazy var historyQueue: OperationQueue = {
172 | let queue = OperationQueue()
173 | queue.maxConcurrentOperationCount = 1
174 | return queue
175 | }()
176 | }
177 |
--------------------------------------------------------------------------------
/SwiftUI/ParticipantView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | A SwiftUI view that manages the participants of a share.
5 |
6 |
7 | */
8 |
9 | import SwiftUI
10 | import CoreData
11 | import CloudKit
12 |
13 | /**
14 | Managing a participant only makes sense when the share exists and is a private share.
15 | A private share is a share whose publicPermission equals to .none.
16 | A public share means a share whose publicPermission is more permissive. Any person who has the share link can
17 | self-add themselves to a public share.
18 | */
19 | struct ParticipantView: View {
20 | @Binding var isPresented: ActiveSheet?
21 | private let share: CKShare
22 |
23 | @State private var toggleProgress: Bool = false
24 | @State private var participants: [Participant]
25 | @State private var wasShareDeleted = false
26 |
27 | private let canUpdateParticipants: Bool
28 |
29 | init(isPresented: Binding, share: CKShare) {
30 | _isPresented = isPresented
31 | self.share = share
32 | participants = share.participants.filter { $0.role != .owner }.map { Participant($0) }
33 |
34 | let privateStore = PersistenceController.shared.privatePersistentStore
35 | canUpdateParticipants = (share.persistentStore == privateStore)
36 | }
37 |
38 | var body: some View {
39 | NavigationView {
40 | VStack {
41 | if wasShareDeleted {
42 | Text("The share was deleted remotely.").padding()
43 | Spacer()
44 | } else {
45 | participantListView()
46 | }
47 | }
48 | .toolbar { toolbarItems() }
49 | .listStyle(PlainListStyle())
50 | .navigationTitle("Participants")
51 | }
52 | .onReceive(NotificationCenter.default.storeDidChangePublisher) { notification in
53 | processStoreChangeNotification(notification)
54 | }
55 | }
56 |
57 | /**
58 | List -> Section header + section content triggers a strange animation when deleting an item.
59 | Moving the header out (like below) fixes the animation issue, but the toolbar item doesn't work on watchOS.
60 | ParticipantListHeader(participants: $participants, share: share)
61 | .padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 0))
62 | List {
63 | SectionContent()
64 | }
65 | */
66 | @ViewBuilder
67 | private func participantListView() -> some View {
68 | ZStack {
69 | List {
70 | Section(header: sectionHeader()) {
71 | sectionContent()
72 | }
73 | }
74 | if toggleProgress {
75 | ProgressView()
76 | }
77 | }
78 | }
79 |
80 | @ViewBuilder
81 | private func sectionHeader() -> some View {
82 | if canUpdateParticipants {
83 | ParticipantListHeader(toggleProgress: $toggleProgress,
84 | participants: $participants, share: share)
85 | } else {
86 | EmptyView()
87 | }
88 | }
89 |
90 | @ViewBuilder
91 | private func sectionContent() -> some View {
92 | ForEach(participants, id: \.self) { participant in
93 | HStack {
94 | Text(participant.ckShareParticipant.userIdentity.lookupInfo?.emailAddress ?? "")
95 | Spacer()
96 | Text(participant.ckShareParticipant.acceptanceStatus.stringValue)
97 | }
98 | }
99 | .onDelete(perform: canUpdateParticipants ? deleteParticipant : nil)
100 | }
101 |
102 | @ToolbarContentBuilder
103 | private func toolbarItems() -> some ToolbarContent {
104 | ToolbarItem(placement: .automatic) {
105 | Button(action: { isPresented = nil }) {
106 | Text("Dismiss")
107 | }
108 | }
109 | /**
110 | "Copy Link" is only available for iOS because watchOS doesn't support UIPasteboard.s
111 | */
112 | #if os(iOS)
113 | ToolbarItem(placement: .bottomBar) {
114 | Button(action: { UIPasteboard.general.url = share.url }) {
115 | Text("Copy Link")
116 | }
117 | }
118 | #endif
119 | }
120 |
121 | private func deleteParticipant(offsets: IndexSet) {
122 | withAnimation {
123 | let ckShareParticipants = offsets.map { participants[$0].ckShareParticipant }
124 | PersistenceController.shared.deleteParticipant(ckShareParticipants, share: share) { share, error in
125 | if error == nil, let updatedShare = share {
126 | participants = updatedShare.participants.filter { $0.role != .owner }.map { Participant($0) }
127 | }
128 | }
129 | }
130 | }
131 |
132 | /**
133 | Ignore the notification in the following cases:
134 | - The notification is not relevant to the private database.
135 | - The notification transaction is not empty. When a share changes, Core Data triggers a store remote change notification with no transaction.
136 | In that case, grab the share with the same title, and use it to update the UI.
137 | */
138 | private func processStoreChangeNotification(_ notification: Notification) {
139 | guard let storeUUID = notification.userInfo?[UserInfoKey.storeUUID] as? String,
140 | storeUUID == PersistenceController.shared.privatePersistentStore.identifier else {
141 | return
142 | }
143 | guard let transactions = notification.userInfo?[UserInfoKey.transactions] as? [NSPersistentHistoryTransaction],
144 | transactions.isEmpty else {
145 | return
146 | }
147 | if let updatedShare = PersistenceController.shared.share(with: share.title) {
148 | participants = updatedShare.participants.filter { $0.role != .owner }.map { Participant($0) }
149 |
150 | } else {
151 | wasShareDeleted = true
152 | }
153 | }
154 | }
155 |
156 | private struct ParticipantListHeader: View {
157 | @Binding var toggleProgress: Bool
158 | @Binding var participants: [Participant]
159 | var share: CKShare
160 | @State private var emailAddress: String = ""
161 |
162 | var body: some View {
163 | HStack {
164 | TextField( "Email", text: $emailAddress)
165 | Button(action: addParticipant) {
166 | Image(systemName: "plus.circle")
167 | .imageScale(.large)
168 | .font(.system(size: 18))
169 | }
170 | .frame(width: 20)
171 | .buttonStyle(.plain)
172 | }
173 | .frame(height: 30)
174 | .padding(5)
175 | .background(Color.listHeaderBackground)
176 | }
177 |
178 | /**
179 | If the participant already exists, no need to do anything.
180 | */
181 | private func addParticipant() {
182 | let isExistingParticipant = share.participants.contains {
183 | $0.userIdentity.lookupInfo?.emailAddress == emailAddress
184 | }
185 | if isExistingParticipant {
186 | return
187 | }
188 |
189 | toggleProgress.toggle()
190 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
191 | PersistenceController.shared.addParticipant(emailAddress: emailAddress, share: share) { share, error in
192 | if error == nil, let updatedShare = share {
193 | DispatchQueue.main.async {
194 | participants = updatedShare.participants.filter { $0.role != .owner }.map { Participant($0) }
195 | emailAddress = ""
196 | toggleProgress.toggle()
197 | }
198 | }
199 | }
200 | }
201 | }
202 | }
203 |
204 | /**
205 | A struct that wraps CKShare.Participant and implements Equatable to trigger SwiftUI update when any of the following state changes:
206 | - userIdentity
207 | - acceptanceStatus
208 | - permission
209 | - role.
210 | */
211 | private struct Participant: Hashable, Equatable {
212 | let ckShareParticipant: CKShare.Participant
213 |
214 | init(_ ckShareParticipant: CKShare.Participant) {
215 | self.ckShareParticipant = ckShareParticipant
216 | }
217 |
218 | static func == (lhs: Participant, rhs: Participant) -> Bool {
219 | let lhsElement = lhs.ckShareParticipant
220 | let rhsElement = rhs.ckShareParticipant
221 |
222 | if lhsElement.userIdentity != rhsElement.userIdentity ||
223 | lhsElement.acceptanceStatus != rhsElement.acceptanceStatus ||
224 | lhsElement.permission != rhsElement.permission ||
225 | lhsElement.role != rhsElement.role {
226 | return false
227 | }
228 | return true
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/Persistence/PersistenceController+Share.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | Extensions that wraps the methods related to sharing.
5 |
6 |
7 | */
8 |
9 | import Foundation
10 | import CoreData
11 | import UIKit
12 | import CloudKit
13 |
14 | #if os(iOS) // UICloudSharingController is only available on iOS
15 | // MARK: - Convenient methods for managing sharing.
16 | //
17 | extension PersistenceController {
18 | func presentCloudSharingController(photo: Photo) {
19 | /**
20 | Grab the share if the photo is already shared.
21 | */
22 | var photoShare: CKShare?
23 | if let shareSet = try? persistentContainer.fetchShares(matching: [photo.objectID]),
24 | let (_, share) = shareSet.first {
25 | photoShare = share
26 | }
27 |
28 | let sharingController: UICloudSharingController
29 | if photoShare == nil {
30 | sharingController = newSharingController(unsharedPhoto: photo, persistenceController: self)
31 | } else {
32 | sharingController = UICloudSharingController(share: photoShare!, container: cloudKitContainer)
33 | }
34 | sharingController.delegate = self
35 | /**
36 | Setting the presentation style to .formSheet so no need to specify sourceView, sourceItem or sourceRect.
37 | */
38 | if let viewController = rootViewController {
39 | sharingController.modalPresentationStyle = .formSheet
40 | viewController.present(sharingController, animated: true)
41 | }
42 | }
43 |
44 | func presentCloudSharingController(share: CKShare) {
45 | let sharingController = UICloudSharingController(share: share, container: cloudKitContainer)
46 | sharingController.delegate = self
47 | /**
48 | Setting the presentation style to .formSheet so no need to specify sourceView, sourceItem or sourceRect.
49 | */
50 | if let viewController = rootViewController {
51 | sharingController.modalPresentationStyle = .formSheet
52 | viewController.present(sharingController, animated: true)
53 | }
54 | }
55 |
56 | private func newSharingController(unsharedPhoto: Photo, persistenceController: PersistenceController) -> UICloudSharingController {
57 | return UICloudSharingController { (_, completion: @escaping (CKShare?, CKContainer?, Error?) -> Void) in
58 | /**
59 | Doesn't specify a share intentionally so Core Data creates a new share (zone).
60 | CloudKit has a limit on how many zones a database can have, so apps should use existing shares if possible to avoid hitting the limit,
61 |
62 | If the share's publicPermission is CKShareParticipantPermissionNone, only private participants can accept the share.
63 | ( Private participants mean the participants an app adds to a share by calling CKShare.addParticipant.)
64 | If the share is more permissive (hence is a public share), anyone with the shareURL can accept (or "self-add" themselves to) it.
65 | The default value of publicPermission is CKShare.ParticipantPermission.none
66 | */
67 | self.persistentContainer.share([unsharedPhoto], to: nil) { objectIDs, share, container, error in
68 | if let share = share {
69 | self.configure(share: share)
70 | }
71 | completion(share, container, error)
72 | }
73 | }
74 | }
75 |
76 | private var rootViewController: UIViewController? {
77 | for scene in UIApplication.shared.connectedScenes {
78 | if scene.activationState == .foregroundActive,
79 | let sceneDeleate = (scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate,
80 | let window = sceneDeleate.window {
81 | return window?.rootViewController
82 | }
83 | }
84 | print("\(#function): Failed to retrieve the window's root view controller.")
85 | return nil
86 | }
87 | }
88 |
89 | extension PersistenceController: UICloudSharingControllerDelegate {
90 | /**
91 | CloudKit triggers the delegate method in two cases:
92 | - A owner stops sharing a share.
93 | - A participant removes themselves from a share by tapping the "Remove Me" button in UICloudSharingController.
94 |
95 | After stopping the sharing, purge the zone or just wait for an import to update the local store.
96 | This sample chooses to purge the zone to avoid stale UI. That triggers a "zone not found" error because UICloudSharingController
97 | has deleted the zone, but doesn't really matter in this context.
98 |
99 | Purging the zone has a caveat:
100 | - When sharing an object from the owner side, Core Data moves the object to the shared zone;
101 | - When calling purgeObjectsAndRecordsInZone, Core Data removes all the objects and records in the zone.
102 | To keep the objects, deep copy the object graph you would like to keep and relate it to an unshared object (relationship).
103 |
104 | The purge API posts an NSPersistentStoreRemoteChange notification after finishing its job, so observe the notification to update
105 | the UI if necessary.
106 | */
107 | //#-code-listing(cloudSharingControllerDidStopSharing)
108 | func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) {
109 | if let share = csc.share {
110 | purgeObjectsAndRecords(with: share)
111 | }
112 | }
113 | //#-end-code-listing
114 |
115 | //#-code-listing(cloudSharingControllerDidSaveShare)
116 | func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
117 | if let share = csc.share, let persistentStore = share.persistentStore {
118 | persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
119 | if let error = error {
120 | print("\(#function): Failed to persist updated share: \(error)")
121 | }
122 | }
123 | }
124 | }
125 | //#-end-code-listing
126 |
127 | func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
128 | print("\(#function): Failed to save a share: \(error)")
129 | }
130 |
131 | func itemTitle(for csc: UICloudSharingController) -> String? {
132 | return csc.share?.title ?? "A cool photo"
133 | }
134 | }
135 | #endif
136 |
137 | #if os(watchOS)
138 | extension PersistenceController {
139 | func presentCloudSharingController(share: CKShare) {
140 | print("\(#function): Cloud sharing controller is unavailable on watchOS.")
141 | }
142 | }
143 | #endif
144 |
145 | extension PersistenceController {
146 |
147 | //#-code-listing(shareObject)
148 | func shareObject(_ unsharedObject: NSManagedObject, to existingShare: CKShare?,
149 | completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)? = nil)
150 | //#-end-code-listing
151 | {
152 | persistentContainer.share([unsharedObject], to: existingShare) { (objectIDs, share, container, error) in
153 | guard error == nil, let share = share else {
154 | print("\(#function): Failed to share an object: \(error!))")
155 | completionHandler?(share, error)
156 | return
157 | }
158 | /**
159 | Deduplicate tags if necessary because adding a photo to an existing share moves the whole object graph to the associated
160 | record zone, which can lead to duplicated tags.
161 | */
162 | if existingShare != nil {
163 | if let tagObjectIDs = objectIDs?.filter({ $0.entity.name == "Tag" }), !tagObjectIDs.isEmpty {
164 | self.deduplicateAndWait(tagObjectIDs: Array(tagObjectIDs))
165 | }
166 | } else {
167 | self.configure(share: share)
168 | }
169 | /**
170 | Synchronize the changes on the share to the private persistent store.
171 | */
172 | self.persistentContainer.persistUpdatedShare(share, in: self.privatePersistentStore) { (share, error) in
173 | if let error = error {
174 | print("\(#function): Failed to persist updated share: \(error)")
175 | }
176 | completionHandler?(share, error)
177 | }
178 | }
179 | }
180 |
181 | /**
182 | Delete the Core Data objects and the records in the CloudKit record zone associcated with the share.
183 | */
184 | func purgeObjectsAndRecords(with share: CKShare, in persistentStore: NSPersistentStore? = nil) {
185 | guard let store = (persistentStore ?? share.persistentStore) else {
186 | print("\(#function): Failed to find the persistent store for share. \(share))")
187 | return
188 | }
189 | persistentContainer.purgeObjectsAndRecordsInZone(with: share.recordID.zoneID, in: store) { (zoneID, error) in
190 | if let error = error {
191 | print("\(#function): Failed to purge objects and records: \(error)")
192 | }
193 | }
194 | }
195 |
196 | func existingShare(photo: Photo) -> CKShare? {
197 | if let shareSet = try? persistentContainer.fetchShares(matching: [photo.objectID]),
198 | let (_, share) = shareSet.first {
199 | return share
200 | }
201 | return nil
202 | }
203 |
204 | func share(with title: String) -> CKShare? {
205 | let stores = [privatePersistentStore, sharedPersistentStore]
206 | let shares = try? persistentContainer.fetchShares(in: stores)
207 | let share = shares?.first(where: { $0.title == title })
208 | return share
209 | }
210 |
211 | func shareTitles() -> [String] {
212 | let stores = [privatePersistentStore, sharedPersistentStore]
213 | let shares = try? persistentContainer.fetchShares(in: stores)
214 | return shares?.map { $0.title } ?? []
215 | }
216 |
217 | private func configure(share: CKShare, with photo: Photo? = nil) {
218 | share[CKShare.SystemFieldKey.title] = "A cool photo"
219 | }
220 | }
221 |
222 | extension PersistenceController {
223 | func addParticipant(emailAddress: String, permission: CKShare.ParticipantPermission = .readWrite, share: CKShare,
224 | completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
225 | /**
226 | Use the email address to look up the participant from the private store. Return if the participant doesn't exist.
227 | Use privatePersistentStore directly because only owner may add participants to a share.
228 | */
229 | let lookupInfo = CKUserIdentity.LookupInfo(emailAddress: emailAddress)
230 | let persistentStore = privatePersistentStore //share.persistentStore!
231 |
232 | persistentContainer.fetchParticipants(matching: [lookupInfo], into: persistentStore) { (results, error) in
233 | guard let participants = results, let participant = participants.first, error == nil else {
234 | completionHandler?(share, error)
235 | return
236 | }
237 |
238 | //#-code-listing(addParticipant)
239 | participant.permission = permission
240 | participant.role = .privateUser
241 | share.addParticipant(participant)
242 |
243 | self.persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
244 | if let error = error {
245 | print("\(#function): Failed to persist updated share: \(error)")
246 | }
247 | completionHandler?(share, error)
248 | }
249 | //#-end-code-listing
250 | }
251 | }
252 |
253 | func deleteParticipant(_ participants: [CKShare.Participant], share: CKShare,
254 | completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
255 | for participant in participants {
256 | share.removeParticipant(participant)
257 | }
258 | /**
259 | Use privatePersistentStore directly because only owner may delete participants to a share.
260 | */
261 | persistentContainer.persistUpdatedShare(share, in: privatePersistentStore) { (share, error) in
262 | if let error = error {
263 | print("\(#function): Failed to persist updated share: \(error)")
264 | }
265 | completionHandler?(share, error)
266 | }
267 | }
268 | }
269 |
270 | extension CKShare.ParticipantAcceptanceStatus {
271 | var stringValue: String {
272 | return ["Unknown", "Pending", "Accepted", "Removed"][rawValue]
273 | }
274 | }
275 |
276 | extension CKShare {
277 | var title: String {
278 | guard let date = creationDate else {
279 | return "Share-\(UUID().uuidString)"
280 | }
281 | let formatter = DateFormatter()
282 | formatter.dateStyle = .short
283 | formatter.timeStyle = .short
284 | return "Share-" + formatter.string(from: date)
285 | }
286 |
287 | var persistentStore: NSPersistentStore? {
288 | let persistentContainer = PersistenceController.shared.persistentContainer
289 | let privatePersistentStore = PersistenceController.shared.privatePersistentStore
290 | if let shares = try? persistentContainer.fetchShares(in: privatePersistentStore) {
291 | let zoneIDs = shares.map { $0.recordID.zoneID }
292 | if zoneIDs.contains(recordID.zoneID) {
293 | return privatePersistentStore
294 | }
295 | }
296 | let sharedPersistentStore = PersistenceController.shared.sharedPersistentStore
297 | if let shares = try? persistentContainer.fetchShares(in: sharedPersistentStore) {
298 | let zoneIDs = shares.map { $0.recordID.zoneID }
299 | if zoneIDs.contains(recordID.zoneID) {
300 | return sharedPersistentStore
301 | }
302 | }
303 | return nil
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sharing Core Data Objects Between iCloud Users
2 | Implement the flow to share data between iCloud users using Core Data CloudKit.
3 |
4 | ## Overview
5 | More and more people own multiple devices and use them to share digital assets or collaborate work. They expect seamless data synchronization across their devices and an easy way to share data with privacy and security in mind. Apps can support such use cases by moving user data to CloudKit and implementing a data sharing flow that includes features like share management and access control.
6 |
7 | This sample app demonstrates how to use Core Data CloudKit to share photos between iCloud users. Users who share photos, called _owners_, can create a share, send out an invitation, manage the permissions, and stop the sharing. Users who accept the share, called _participants_, can view or edit the photos, or stop participating the share.
8 |
9 | ## Configure the Sample Code Project
10 | Before building the sample app, perform the following steps in Xcode:
11 | 1. In the General pane of the `CoreDataCloudKitShare` target, update the Bundle Identifier field with a new identifier.
12 | 2. In the Signing & Capabilities pane, select the applicable team from the Team drop-down menu to let Xcode automatically manage the provisioning profile. See [Assign a project to a team](https://help.apple.com/xcode/mac/current/#/dev23aab79b4) for details.
13 | 3. Make sure the iCloud capability is present and the CloudKit option is in a selected state, then select the iCloud container with your bundle identifier from step 1 from the Containers list. If the container doesn’t exist, click the Add button (+), enter the container name (iCloud.<*bundle identifier*>), and click OK to let Xcode create the container and associate it with the app.
14 | 4. If you prefer using an existing container, select it from the Containers list.
15 | 5. Specify your iCloud container for the `gCloudKitContainerIdentifier` variable in PersistenceController.swift. An iCloud container identifier is case-sensitive and must begin with "`iCloud.`".
16 | 6. Similar to step 1, change the bundle identifiers and the developer team for the WatchKit app and WatchKit Extension targets. The bundle identifiers must be `.watchkitapp` and `.watchkitapp.watchkitextension` respectively.
17 | 7. Similar to step 2, specify the iCloud container for the WatchKit Extension target. To synchronize data across iCloud, the iOS app and WatchKit extension must share the same iCloud container.
18 | 8. Open the Info.plist file of the WatchKit app target, then change the value of WKCompanionAppBundleIdentifier key to ``.
19 | 9. Open the Info.plist file of the WatchKit Extension target, then change the value of NSExtension > NSExtensionAttributes > WKAppBundleIdentifier key to `.watchkitapp`.
20 |
21 | To run the sample app on a device, configure the device as follows:
22 | 1. Log in with an Apple ID. For the CloudKit private database to synchronize, the Apple ID must be the same on the devices. (For an Apple Watch, log in at the Watch app on the paired iPhone, then make sure the Apple ID shows up on the Settings app on the watch.)
23 | 2. For an iOS device, choose Settings > Apple ID > iCloud, and turn on iCloud Drive, if it is off.
24 | 3. After running the sample app on the device, go to Settings > Notifications, and make sure “Allow Notifications” is on. For an Apple Watch, use the Watch app on the paired iPhone to make sure that notifications are on for the app.
25 |
26 | To create and configure a new project that uses Core Data CloudKit, see [Setting Up Core Data with CloudKit](https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/setting_up_core_data_with_cloudkit?changes=__3).
27 |
28 | ## Create the CloudKit Schema for Apps
29 | CloudKit apps must have a schema to declare the data types they use. When apps create a record in the CloudKit development environment, CloudKit automatically creates the record type if it doesn't exist. In the production environment, CloudKit doesn't have that capability, nor does it allow removing an existing record type or field, so after finalizing the schema, be sure to deploy it to the production environment. Without doing that, apps that work in the production environment, like the App Store or TestFlight ones, would not work. For more information, see [Deploying an iCloud Container’s Schema](https://developer.apple.com/documentation/cloudkit/managing_icloud_containers_with_the_cloudkit_database_app/deploying_an_icloud_container_s_schema).
30 |
31 | Core Data CloudKit apps can use [`initializeCloudKitSchema(options:)`](https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontainer/3343548-initializecloudkitschema) to create the CloudKit schema that matches their Core Data model, or keep it up to date every time their model changes. The method works by creating fake data for the record types and then delete it, which can take some time and blocks the other CloudKit operations. Apps must not call it in the production environment, or in the normal development process that doesn't include model changes.
32 |
33 | To create the CloudKit schema for this sample app, pick the "InitializeCloudKitSchema" target from Xcode's target menu, and run it. Having a target dedicated on CloudKit schema creation separates the `initializeCloudKitSchema(options:)` call from the normal flow. After running the target, be sure to check with [CloudKit Console](http://icloud.developer.apple.com/dashboard/) if every Core Data entity and attribute has a CloudKit counterpart. See [Reading CloudKit Records for Core Data](https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/reading_cloudkit_records_for_core_data) for the detailed mapping rules.
34 |
35 | For apps that use CloudKit public database, manually add a `Queryable` index for the `recordName` and `modifiedAt` fields of all record types, including the `CDMR` type that Core Data generates to manage many-to-many relationships.
36 |
37 | For more information on this topic, see [Creating a Core Data Model for CloudKit](https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/creating_a_core_data_model_for_cloudkit)
38 |
39 | ## Try out the Sharing Flow With the Sample App
40 | To create and share a photo using the sample app, follow these steps:
41 | 1. Prepare two iOS devices, A and B, and log in with a different Apple ID.
42 | 2. Use Xcode to build and run the sample app on the devices.
43 | 3. On device A, tap the Add(+) button to show the photo picker, then pick a photo and add it to the Core Data store.
44 | 4. Long press the photo to show the action menu, then tap the "Create New Share" button to present the CloudKit sharing UI.
45 | 5. Follow the UI to send a link to the Apple ID on device B. Try to use iMessage because it's easier to set up.
46 | 6. After receiving the link on device B, tap it to accept and open the share, which launches the sample app and shows the photo.
47 |
48 | To discover more features of the sample app:
49 | - On device A, add another photo, long press it and tap the "Add to Existing Share" button, then pick a share and tap the "Add" button. See the photo soon appears on Device B.
50 | - On device B, long press the photo, tap the "Manage Participation" button to present the CloudKit sharing UI, then pick the Apple ID that has "(Me)" suffix and tap "Remove Me" to remove the participation. See the photo disappears.
51 | - Tap the "Manage Shares" button, then pick the share, and try to manage its participants using [`UICloudSharingController`](https://developer.apple.com/documentation/uikit/uicloudsharingcontroller) or the app UI.
52 |
53 | It may take some time (minutes or longer) for one user to see the changes from the others. Core Data CloudKit is not for real-time synchronization. When users change the store on their device, it is up to the system to determine when to synchronize the change. There is no API for apps to speed up, slow down, or choose the timing for the synchronization.
54 |
55 | ## Set up the Core Data Stack
56 | Every CloudKit container has a [private database](https://developer.apple.com/documentation/cloudkit/ckcontainer/1399205-privateclouddatabase) and a [shared database](https://developer.apple.com/documentation/cloudkit/ckcontainer/1640408-sharedclouddatabase). To mirror these databases, set up a Core Data stack with two stores, and set the store's [database scope](https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontaineroptions/3580372-databasescope?changes=__3) to `.private` and `.shared` respectively.
57 |
58 | When setting up the store description, enable [persistent history](https://developer.apple.com/documentation/coredata/persistent_history) tracking and turn on remote change notifications by setting the `NSPersistentHistoryTrackingKey` and `NSPersistentStoreRemoteChangeNotificationPostOptionKey` options to `true`. Core Data relies on the persistent history to track the store changes, and apps need to update their UI when remote changes occur.
59 |
60 | - CodeListing: setOption
61 |
62 | For apps (under the same developer team) to synchronize data through CloudKit, they must use the same CloudKit container. This sample app explicitly specifies the same container for its iOS and watchOS apps when setting up the CloudKit container options:
63 |
64 | - CodeListing: NSPersistentCloudKitContainerOptions
65 |
66 | ## Share a Core Data object
67 | Sharing a Core Data object between iCloud users includes the following tasks:
68 | 1. On the owner side, create a share with an appropriate permission.
69 | 2. Invite participants by making the share link available to them.
70 | 3. On the participant side, accept the share.
71 | 4. On both sides, manage shares. Owners can stop sharing the object, change the share permission for a participant. Participants can stop their participation.
72 |
73 | [`NSPersistentCloudKitContainer`](https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontainer?changes=__3) provides methods for creating a share ([`CKShare`](https://developer.apple.com/documentation/cloudkit/ckshare)) for Core Data objects and managing the interaction between the share and the associated objects. `UICloudSharingController` implements the share invitation and management. Apps can implement a sharing flow using these two APIs.
74 |
75 | To create a share for Core Data objects, call [`share(_:to:completion:)`](https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontainer/3746834-share?changes=__3). Apps can choose creating a new share, or adding the objects to an existing share. Core Data uses CloudKit zone sharing so each share has its own record zone on the CloudKit server. (For more details, see [WWDC21 session 10015: Build Apps that Share Data Through CloudKit and Core Data](https://developer.apple.com/videos/play/wwdc2021/10015/) and [WWDC21 session 10086: What's new in CloudKit](https://developer.apple.com/videos/play/wwdc2021/10086).) CloudKit has a limit on how many record zones a database can have. To avoid hitting the limit, consider using an existing share if appropriate.
76 |
77 | See the following method for how this sample app shares a photo:
78 | - CodeListing: shareObject
79 |
80 | `NSPersistentCloudKitContainer` doesn't automatically handle the changes `UICloudSharingController` (or other CloudKit APIs) makes on a share. When the kind of changes happen, apps must update the Core Data store by calling [`persistUpdatedShare(_:in:completion:)`](https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontainer/3746832-persistupdatedshare?changes=__3). The sample app implements the following [`UICloudSharingControllerDelegate`](https://developer.apple.com/documentation/uikit/uicloudsharingcontrollerdelegate) method to persist a updated share.
81 |
82 | - CodeListing: cloudSharingControllerDidSaveShare
83 |
84 | Similarly, when owners tap the "Stop Sharing" button or participants tap the "Remove Me" button in the CloudKit sharing UI, `NSPersistentCloudKitContainer` doesn't immediately know the change. To avoid stale UI in this case, implement the following delegate method to purge the Core Data objects and CloudKit records associated with the share using [`purgeObjectsAndRecordsInZone(with:in:completion:)`](https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontainer/3746833-purgeobjectsandrecordsinzone?changes=__3).
85 |
86 | - CodeListing: cloudSharingControllerDidStopSharing
87 |
88 | Core Data doesn't support cross-share relationships. That is, it doesn't allow relating objects associated with different shares. When sharing an object, Core Data moves the whole object graph (including the object and all its relationships) to the share's record zone. When users stop a share, Core Data deletes the object graph. In the case where apps need to reserve the data when users stopping a share, make a deep copy of the object graph and make sure no object in the graph is associated with any share.
89 |
90 | ## Detect Relevant Changes by Consuming Store Persistent History
91 | When importing data from CloudKit, `NSPersistentCloudKitContainer` records the changes on Core Data objects in the store's persistent history, and triggers remote change notifications (`.NSPersistentStoreRemoteChange`) so apps can keep their state up to date if necessary. The sample app observes the notification and does the followings in the notification handler:
92 |
93 | - Gather the relevant history transactions ([`NSPersistentHistoryTransaction`](https://developer.apple.com/documentation/coredata/nspersistenthistorytransaction)), and notify the views that remote changes happen. Note that the changes on shares don't generate any transactions.
94 | - The views that present photos merge the transactions to the `viewContext` of the persistent container, which triggers a SwiftUI update. Views relevant to shares fetch the shares from the stores, and update with them.
95 | - Detect the new tags from CloudKit, and remove duplicate tags if necessary.
96 |
97 | To process the persistent history more effectively, the app:
98 | - Maintains the token of the last transaction it consumes for each store, and uses it as the starting point of next run.
99 | - Maintains a transaction author, and uses it to filter the transactions irrelevant to Core Data CloudKit.
100 | - Only fetches and consumes the history of the relevant persistent store.
101 |
102 | This is the code that sets up the history fetch request (`NSPersistentHistoryChangeRequest`):
103 | - CodeListing: fetchHistory
104 |
105 | For more information about persistent history processing, see [Consuming Relevant Store Changes](https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes).
106 |
107 | ## Remove Duplicate Data
108 | In the CloudKit environment, duplicate data is sometimes inevitable:
109 | - Different peers can create same data. In this sample app, owners can share a photo with a permission that allows participants to tag it. When owners and participants simultaneously create a same tag, a duplicate occurs.
110 | - Apps rely on some initial data and there is no way to allow only one peer to preload it. Duplicates occur when multiple peers preload the data at the same time.
111 |
112 | To remove duplicate data (or _deduplicate_), implement a way that allows all peers to eventually reserve the same winner and remove others. The sample app removes duplicate tags in the following way:
113 |
114 | 1. Give every tag a universally unique identifier (UUID). Tags that meet the following criteria are duplicates and only one should exist:
115 | - They have a same tag name. (Their UUIDs are still different.)
116 | - They are associated with a same share, and so are in the same CloudKit record zone.
117 | 2. Detect new tags from CloudKit by looking into the persistent history every time a remote change notification occurs.
118 | 3. For each new tag, fetch the duplicates from the same persistent store, and sort them with their UUID so the tag with the smallest UUID goes first.
119 | 4. Pick the first tag as the winner and remove the others. Because UUID is globally unique and every peer picks the first tag, all peers eventually reach to the same winner, which is the tag that has the globally smallest UUID.
120 |
121 | The sample app only detects and removes duplicate tags from the owner side because participants may not have write permission. That is, deduplication only applies to the private persistent store.
122 |
123 | See the following method for the code that deduplicate tags:
124 |
125 | - CodeListing: deduplicateAndWait
126 |
127 | ## Implement a Custom Sharing Flow
128 | When `UICloudSharingController` is unavailable or doesn't fit the app UI, consider implementing a custom sharing flow if necessary. (`UICloudSharingController` is unavailabe on watchOS. On macOS, use [`NSSharingService`](https://developer.apple.com/documentation/appkit/nssharingservice) with the [`.cloudSharing`](https://developer.apple.com/documentation/appkit/nssharingservice/name/1644670-cloudsharing) service.) To do that, here are the steps and relevant APIs:
129 |
130 | 1. On the owner side, pick the Core Data objects to share, and create a share with them using `share(_:to:completion:)`.
131 | 2. Configure the share with appropriate permissions, and add participants if it's a private share.
132 | A share is private if its [`publicPermission`](https://developer.apple.com/documentation/cloudkit/ckshare/1640494-publicpermission) is more permissive than [`.none`](https://developer.apple.com/documentation/cloudkit/ckshare/participantpermission/none). For shares that have `.none` public permission (called _public shares_), users can participate by tapping the share link, hence no need to add participants beforehand. Look up the participants using [`fetchParticipants(matching:into:completion:)`](https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontainer/3746829-fetchparticipants) or [`CKFetchShareParticipantsOperation`](https://developer.apple.com/documentation/cloudkit/ckfetchshareparticipantsoperation), then add them to the share by calling [`addParticipant(_:)`](https://developer.apple.com/documentation/cloudkit/ckshare/1640443-addparticipant). Configure the participant permission using [`CKShare.ParticipantPermission`](https://developer.apple.com/documentation/cloudkit/ckshare/participant/1640433-permission).
133 | 3. Implement a mechanism for the owner to deliver the share link ([`CKShare.url`](https://developer.apple.com/documentation/cloudkit/ckshare/1640465-url)).
134 | 4. On the participant side, accept the share.
135 | After receiving the share link, participants tap it to accept the share and open the app. The system calls [`windowScene(_:userDidAcceptCloudKitShareWith:)`](https://developer.apple.com/documentation/uikit/uiwindowscenedelegate/3238089-windowscene) (or [`userDidAcceptCloudKitShare(with:)`](https://developer.apple.com/documentation/watchkit/wkextensiondelegate/3612144-userdidacceptcloudkitshare) on watchOS) when launching the app in this context, and the app accepts the share using [`acceptShareInvitations(from:into:completion:)`](https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontainer/3746828-acceptshareinvitations) or [`CKAcceptSharesOperation`](https://developer.apple.com/documentation/cloudkit/ckacceptsharesoperation). After the acceptance synchronizes, the objects the owner shares are available in the participant's store that mirrors the CloudKit shared database.
136 | 5. On the owner side, manage the participants of the share using `addParticipant(_:)` and `removeParticipant(_:)`, or stop the sharing by calling `purgeObjectsAndRecordsInZone(with:in:completion:)`.
137 | 6. On the participant side, stop the participation by calling `purgeObjectsAndRecordsInZone(with:in:completion:)`.
138 |
139 | In the whole process, whenever changing a share using CloudKit APIs, call `persistUpdatedShare(_:in:completion:)` so Core Data persists the change to the store and synchronize it with CloudKit. As an example, this sample uses the following code to add a participant
140 |
141 | - CodeListing: addParticipant
142 |
143 | - Note: To be able to accept a share when users tap a share link, the app's `info.plist` file must contain the `CKSharingSupported` key and its value must be `true`.
144 |
--------------------------------------------------------------------------------
/CoreDataCloudKitShare.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 401C06ED276E9D790024D92C /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C06EC276E9D790024D92C /* ExtensionDelegate.swift */; };
11 | 4026B868270A19390060F0F7 /* TaggingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4026B867270A19390060F0F7 /* TaggingView.swift */; };
12 | 403177D32742D5790048F2DC /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40527E16270BBE3400DB2901 /* RatingView.swift */; };
13 | 403177D42742DB270048F2DC /* TaggingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4026B867270A19390060F0F7 /* TaggingView.swift */; };
14 | 403177D62742F4B40048F2DC /* PhotoGridItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D622703A27000D2BA90 /* PhotoGridItemView.swift */; };
15 | 403177D82742F4F70048F2DC /* SwiftUIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403177D72742F4F70048F2DC /* SwiftUIHelper.swift */; };
16 | 403177D92742F4F70048F2DC /* SwiftUIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403177D72742F4F70048F2DC /* SwiftUIHelper.swift */; };
17 | 403177DA27430A800048F2DC /* PhotoGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7CED27024EBD00D2BA90 /* PhotoGridView.swift */; };
18 | 403177DB27430E340048F2DC /* ParticipantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40744288270FAFB3009CABC7 /* ParticipantView.swift */; };
19 | 403177DC274310690048F2DC /* AddToExistingShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408BD4D92713B29900294A81 /* AddToExistingShareView.swift */; };
20 | 403177DD274310B50048F2DC /* ManagingSharesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408BD4DB2713F0F400294A81 /* ManagingSharesView.swift */; };
21 | 403177DE27432CE40048F2DC /* PhotoContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4035B60D27150DA100F46D6B /* PhotoContextMenu.swift */; };
22 | 4035B60E27150DA100F46D6B /* PhotoContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4035B60D27150DA100F46D6B /* PhotoContextMenu.swift */; };
23 | 40527E17270BBE3400DB2901 /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40527E16270BBE3400DB2901 /* RatingView.swift */; };
24 | 40527E19270BC8FE00DB2901 /* PersistenceController+Rating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40527E18270BC8FE00DB2901 /* PersistenceController+Rating.swift */; };
25 | 4058316A2767DCDE0044E86D /* FullImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405831692767DCDE0044E86D /* FullImageView.swift */; };
26 | 4058316B2767F0ED0044E86D /* FullImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405831692767DCDE0044E86D /* FullImageView.swift */; };
27 | 40744289270FAFB3009CABC7 /* ParticipantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40744288270FAFB3009CABC7 /* ParticipantView.swift */; };
28 | 4074428B270FB2B5009CABC7 /* PersistenceController+Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4074428A270FB2B5009CABC7 /* PersistenceController+Share.swift */; };
29 | 407A0CDE2707D0CD00F481C5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D6A2704334400D2BA90 /* SceneDelegate.swift */; };
30 | 407A0CE02707D0CD00F481C5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D6C2704335A00D2BA90 /* AppDelegate.swift */; };
31 | 407A0CE12707D0CD00F481C5 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D0027024F5D00D2BA90 /* PersistenceController.swift */; };
32 | 407A0CEA2707D0CD00F481C5 /* CoreDataCloudKitShare.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 407D7CF627024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodeld */; };
33 | 407A0CED2707D0CD00F481C5 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 407D7D092702502400D2BA90 /* CloudKit.framework */; };
34 | 407A0CEF2707D0CD00F481C5 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 407D7CF227024EBE00D2BA90 /* Preview Assets.xcassets */; };
35 | 407A0CF02707D0CD00F481C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 407D7CEF27024EBE00D2BA90 /* Assets.xcassets */; };
36 | 407D7CEC27024EBD00D2BA90 /* CoreDataCloudKitShareApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7CEB27024EBD00D2BA90 /* CoreDataCloudKitShareApp.swift */; };
37 | 407D7CEE27024EBD00D2BA90 /* PhotoGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7CED27024EBD00D2BA90 /* PhotoGridView.swift */; };
38 | 407D7CF027024EBE00D2BA90 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 407D7CEF27024EBE00D2BA90 /* Assets.xcassets */; };
39 | 407D7CF327024EBE00D2BA90 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 407D7CF227024EBE00D2BA90 /* Preview Assets.xcassets */; };
40 | 407D7CF827024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 407D7CF627024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodeld */; };
41 | 407D7D0427024F5D00D2BA90 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D0027024F5D00D2BA90 /* PersistenceController.swift */; };
42 | 407D7D0A2702502400D2BA90 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 407D7D092702502400D2BA90 /* CloudKit.framework */; };
43 | 407D7D102702556700D2BA90 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 407D7D0F2702556700D2BA90 /* Assets.xcassets */; };
44 | 407D7D162702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 407D7D152702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
45 | 407D7D1B2702556800D2BA90 /* CoreDataCloudKitShareApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D1A2702556800D2BA90 /* CoreDataCloudKitShareApp.swift */; };
46 | 407D7D252702556900D2BA90 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 407D7D242702556900D2BA90 /* Assets.xcassets */; };
47 | 407D7D282702556900D2BA90 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 407D7D272702556900D2BA90 /* Preview Assets.xcassets */; };
48 | 407D7D2D2702556900D2BA90 /* CoreDataCloudKitShareOnWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 407D7D0D2702556600D2BA90 /* CoreDataCloudKitShareOnWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
49 | 407D7D39270262EF00D2BA90 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 407D7D38270262EF00D2BA90 /* CloudKit.framework */; };
50 | 407D7D3B2702A2A400D2BA90 /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D3A2702A2A400D2BA90 /* PhotoPicker.swift */; };
51 | 407D7D5F2703878000D2BA90 /* PersistenceController+Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D5E2703878000D2BA90 /* PersistenceController+Photo.swift */; };
52 | 407D7D612703880E00D2BA90 /* PersistenceController+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D602703880E00D2BA90 /* PersistenceController+Tag.swift */; };
53 | 407D7D632703A27000D2BA90 /* PhotoGridItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D622703A27000D2BA90 /* PhotoGridItemView.swift */; };
54 | 407D7D6B2704334400D2BA90 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D6A2704334400D2BA90 /* SceneDelegate.swift */; };
55 | 407D7D6D2704335A00D2BA90 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D6C2704335A00D2BA90 /* AppDelegate.swift */; };
56 | 408BD4DA2713B29900294A81 /* AddToExistingShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408BD4D92713B29900294A81 /* AddToExistingShareView.swift */; };
57 | 408BD4DC2713F0F400294A81 /* ManagingSharesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408BD4DB2713F0F400294A81 /* ManagingSharesView.swift */; };
58 | 40AE53F12719F98C00B978CA /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AE53F02719F98C00B978CA /* CoreDataHelper.swift */; };
59 | 40B6201F273AFD3400B27D3D /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AE53F02719F98C00B978CA /* CoreDataHelper.swift */; };
60 | 40B62020273AFD3700B27D3D /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D0027024F5D00D2BA90 /* PersistenceController.swift */; };
61 | 40B62021273AFD6700B27D3D /* CoreDataCloudKitShare.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 407D7CF627024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodeld */; };
62 | 40B62022273AFD9300B27D3D /* PersistenceController+Deduplicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D68E932719101A00FB9B78 /* PersistenceController+Deduplicate.swift */; };
63 | 40B62023273AFD9700B27D3D /* PersistenceController+Rating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40527E18270BC8FE00DB2901 /* PersistenceController+Rating.swift */; };
64 | 40B62024273AFD9900B27D3D /* PersistenceController+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D602703880E00D2BA90 /* PersistenceController+Tag.swift */; };
65 | 40B62027273AFDC600B27D3D /* PersistenceController+Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7D5E2703878000D2BA90 /* PersistenceController+Photo.swift */; };
66 | 40BACC0B2740646E00F12CBD /* PersistenceController+Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4074428A270FB2B5009CABC7 /* PersistenceController+Share.swift */; };
67 | 40BF6728273B0B7400ED9D2D /* CoreDataCloudKitShareApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407D7CEB27024EBD00D2BA90 /* CoreDataCloudKitShareApp.swift */; };
68 | 40BF672E273B17A500ED9D2D /* PersistenceController+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40BF672C273B17A200ED9D2D /* PersistenceController+History.swift */; };
69 | 40BF672F273B17A900ED9D2D /* PersistenceController+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40BF672C273B17A200ED9D2D /* PersistenceController+History.swift */; };
70 | 40C6211E2755AFB500015301 /* SharePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40C6211D2755AFB500015301 /* SharePickerView.swift */; };
71 | 40C6211F2755AFB500015301 /* SharePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40C6211D2755AFB500015301 /* SharePickerView.swift */; };
72 | 40D68E942719101A00FB9B78 /* PersistenceController+Deduplicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D68E932719101A00FB9B78 /* PersistenceController+Deduplicate.swift */; };
73 | /* End PBXBuildFile section */
74 |
75 | /* Begin PBXContainerItemProxy section */
76 | 407D7D172702556800D2BA90 /* PBXContainerItemProxy */ = {
77 | isa = PBXContainerItemProxy;
78 | containerPortal = 407D7CE027024EBD00D2BA90 /* Project object */;
79 | proxyType = 1;
80 | remoteGlobalIDString = 407D7D142702556800D2BA90;
81 | remoteInfo = "CoreDataCloudKitShareOnWatch WatchKit Extension";
82 | };
83 | 407D7D2B2702556900D2BA90 /* PBXContainerItemProxy */ = {
84 | isa = PBXContainerItemProxy;
85 | containerPortal = 407D7CE027024EBD00D2BA90 /* Project object */;
86 | proxyType = 1;
87 | remoteGlobalIDString = 407D7D0C2702556600D2BA90;
88 | remoteInfo = CoreDataCloudKitShareOnWatch;
89 | };
90 | /* End PBXContainerItemProxy section */
91 |
92 | /* Begin PBXCopyFilesBuildPhase section */
93 | 407A0CF12707D0CD00F481C5 /* Embed Watch Content */ = {
94 | isa = PBXCopyFilesBuildPhase;
95 | buildActionMask = 2147483647;
96 | dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
97 | dstSubfolderSpec = 16;
98 | files = (
99 | );
100 | name = "Embed Watch Content";
101 | runOnlyForDeploymentPostprocessing = 0;
102 | };
103 | 407D7D2E2702556900D2BA90 /* Embed Watch Content */ = {
104 | isa = PBXCopyFilesBuildPhase;
105 | buildActionMask = 2147483647;
106 | dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
107 | dstSubfolderSpec = 16;
108 | files = (
109 | 407D7D2D2702556900D2BA90 /* CoreDataCloudKitShareOnWatch.app in Embed Watch Content */,
110 | );
111 | name = "Embed Watch Content";
112 | runOnlyForDeploymentPostprocessing = 0;
113 | };
114 | 407D7D312702556900D2BA90 /* Embed App Extensions */ = {
115 | isa = PBXCopyFilesBuildPhase;
116 | buildActionMask = 2147483647;
117 | dstPath = "";
118 | dstSubfolderSpec = 13;
119 | files = (
120 | 407D7D162702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension.appex in Embed App Extensions */,
121 | );
122 | name = "Embed App Extensions";
123 | runOnlyForDeploymentPostprocessing = 0;
124 | };
125 | /* End PBXCopyFilesBuildPhase section */
126 |
127 | /* Begin PBXFileReference section */
128 | 401C06EC276E9D790024D92C /* ExtensionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = ""; };
129 | 4026B867270A19390060F0F7 /* TaggingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaggingView.swift; sourceTree = ""; };
130 | 403177D72742F4F70048F2DC /* SwiftUIHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHelper.swift; sourceTree = ""; };
131 | 4035B60D27150DA100F46D6B /* PhotoContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoContextMenu.swift; sourceTree = ""; };
132 | 40527E16270BBE3400DB2901 /* RatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingView.swift; sourceTree = ""; };
133 | 40527E18270BC8FE00DB2901 /* PersistenceController+Rating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistenceController+Rating.swift"; sourceTree = ""; };
134 | 405831692767DCDE0044E86D /* FullImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullImageView.swift; sourceTree = ""; };
135 | 406193C22755686C006EC5D8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
136 | 40744288270FAFB3009CABC7 /* ParticipantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantView.swift; sourceTree = ""; };
137 | 4074428A270FB2B5009CABC7 /* PersistenceController+Share.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistenceController+Share.swift"; sourceTree = ""; };
138 | 407A0CF62707D0CD00F481C5 /* InitializeCloudKitSchema.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InitializeCloudKitSchema.app; sourceTree = BUILT_PRODUCTS_DIR; };
139 | 407D7CE827024EBD00D2BA90 /* CoreDataCloudKitShare.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CoreDataCloudKitShare.app; sourceTree = BUILT_PRODUCTS_DIR; };
140 | 407D7CEB27024EBD00D2BA90 /* CoreDataCloudKitShareApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataCloudKitShareApp.swift; sourceTree = ""; };
141 | 407D7CED27024EBD00D2BA90 /* PhotoGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridView.swift; sourceTree = ""; };
142 | 407D7CEF27024EBE00D2BA90 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
143 | 407D7CF227024EBE00D2BA90 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
144 | 407D7CF727024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataCloudKitShare.xcdatamodel; sourceTree = ""; };
145 | 407D7CFE27024F4100D2BA90 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
146 | 407D7D0027024F5D00D2BA90 /* PersistenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; };
147 | 407D7D072702502200D2BA90 /* CoreDataCloudKitShare.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CoreDataCloudKitShare.entitlements; sourceTree = ""; };
148 | 407D7D092702502400D2BA90 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
149 | 407D7D0D2702556600D2BA90 /* CoreDataCloudKitShareOnWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CoreDataCloudKitShareOnWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
150 | 407D7D0F2702556700D2BA90 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
151 | 407D7D152702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "CoreDataCloudKitShareOnWatch WatchKit Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
152 | 407D7D1A2702556800D2BA90 /* CoreDataCloudKitShareApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataCloudKitShareApp.swift; sourceTree = ""; };
153 | 407D7D242702556900D2BA90 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
154 | 407D7D272702556900D2BA90 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
155 | 407D7D292702556900D2BA90 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
156 | 407D7D36270262C500D2BA90 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
157 | 407D7D37270262ED00D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "CoreDataCloudKitShareOnWatch WatchKit Extension.entitlements"; sourceTree = ""; };
158 | 407D7D38270262EF00D2BA90 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS8.0.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; };
159 | 407D7D3A2702A2A400D2BA90 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; };
160 | 407D7D5E2703878000D2BA90 /* PersistenceController+Photo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistenceController+Photo.swift"; sourceTree = ""; };
161 | 407D7D602703880E00D2BA90 /* PersistenceController+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistenceController+Tag.swift"; sourceTree = ""; };
162 | 407D7D622703A27000D2BA90 /* PhotoGridItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridItemView.swift; sourceTree = ""; };
163 | 407D7D6A2704334400D2BA90 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
164 | 407D7D6C2704335A00D2BA90 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
165 | 408BD4D92713B29900294A81 /* AddToExistingShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToExistingShareView.swift; sourceTree = ""; };
166 | 408BD4DB2713F0F400294A81 /* ManagingSharesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagingSharesView.swift; sourceTree = ""; };
167 | 40AE53F02719F98C00B978CA /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = ""; };
168 | 40BF672C273B17A200ED9D2D /* PersistenceController+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistenceController+History.swift"; sourceTree = ""; };
169 | 40C6211D2755AFB500015301 /* SharePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePickerView.swift; sourceTree = ""; };
170 | 40D68E932719101A00FB9B78 /* PersistenceController+Deduplicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistenceController+Deduplicate.swift"; sourceTree = ""; };
171 | /* End PBXFileReference section */
172 |
173 | /* Begin PBXFrameworksBuildPhase section */
174 | 407A0CEC2707D0CD00F481C5 /* Frameworks */ = {
175 | isa = PBXFrameworksBuildPhase;
176 | buildActionMask = 2147483647;
177 | files = (
178 | 407A0CED2707D0CD00F481C5 /* CloudKit.framework in Frameworks */,
179 | );
180 | runOnlyForDeploymentPostprocessing = 0;
181 | };
182 | 407D7CE527024EBD00D2BA90 /* Frameworks */ = {
183 | isa = PBXFrameworksBuildPhase;
184 | buildActionMask = 2147483647;
185 | files = (
186 | 407D7D0A2702502400D2BA90 /* CloudKit.framework in Frameworks */,
187 | );
188 | runOnlyForDeploymentPostprocessing = 0;
189 | };
190 | 407D7D122702556800D2BA90 /* Frameworks */ = {
191 | isa = PBXFrameworksBuildPhase;
192 | buildActionMask = 2147483647;
193 | files = (
194 | 407D7D39270262EF00D2BA90 /* CloudKit.framework in Frameworks */,
195 | );
196 | runOnlyForDeploymentPostprocessing = 0;
197 | };
198 | /* End PBXFrameworksBuildPhase section */
199 |
200 | /* Begin PBXGroup section */
201 | 403177D52742DF030048F2DC /* SwiftUI */ = {
202 | isa = PBXGroup;
203 | children = (
204 | 403177D72742F4F70048F2DC /* SwiftUIHelper.swift */,
205 | 407D7CED27024EBD00D2BA90 /* PhotoGridView.swift */,
206 | 407D7D622703A27000D2BA90 /* PhotoGridItemView.swift */,
207 | 4035B60D27150DA100F46D6B /* PhotoContextMenu.swift */,
208 | 4026B867270A19390060F0F7 /* TaggingView.swift */,
209 | 40527E16270BBE3400DB2901 /* RatingView.swift */,
210 | 40744288270FAFB3009CABC7 /* ParticipantView.swift */,
211 | 408BD4D92713B29900294A81 /* AddToExistingShareView.swift */,
212 | 408BD4DB2713F0F400294A81 /* ManagingSharesView.swift */,
213 | 40C6211D2755AFB500015301 /* SharePickerView.swift */,
214 | 405831692767DCDE0044E86D /* FullImageView.swift */,
215 | );
216 | path = SwiftUI;
217 | sourceTree = "";
218 | };
219 | 407D7CDF27024EBD00D2BA90 = {
220 | isa = PBXGroup;
221 | children = (
222 | 407D7CFE27024F4100D2BA90 /* README.md */,
223 | 407D7CFF27024F5D00D2BA90 /* Persistence */,
224 | 403177D52742DF030048F2DC /* SwiftUI */,
225 | 407D7CEA27024EBD00D2BA90 /* CoreDataCloudKitShare */,
226 | 407D7D0E2702556600D2BA90 /* CoreDataCloudKitShareOnWatch */,
227 | 407D7D192702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension */,
228 | 407D7CE927024EBD00D2BA90 /* Products */,
229 | 407D7D082702502400D2BA90 /* Frameworks */,
230 | );
231 | sourceTree = "";
232 | };
233 | 407D7CE927024EBD00D2BA90 /* Products */ = {
234 | isa = PBXGroup;
235 | children = (
236 | 407D7CE827024EBD00D2BA90 /* CoreDataCloudKitShare.app */,
237 | 407D7D0D2702556600D2BA90 /* CoreDataCloudKitShareOnWatch.app */,
238 | 407D7D152702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension.appex */,
239 | 407A0CF62707D0CD00F481C5 /* InitializeCloudKitSchema.app */,
240 | );
241 | name = Products;
242 | sourceTree = "";
243 | };
244 | 407D7CEA27024EBD00D2BA90 /* CoreDataCloudKitShare */ = {
245 | isa = PBXGroup;
246 | children = (
247 | 407D7D072702502200D2BA90 /* CoreDataCloudKitShare.entitlements */,
248 | 407D7D3A2702A2A400D2BA90 /* PhotoPicker.swift */,
249 | 407D7CEB27024EBD00D2BA90 /* CoreDataCloudKitShareApp.swift */,
250 | 407D7D6C2704335A00D2BA90 /* AppDelegate.swift */,
251 | 407D7D6A2704334400D2BA90 /* SceneDelegate.swift */,
252 | 407D7D36270262C500D2BA90 /* Info.plist */,
253 | 407D7CEF27024EBE00D2BA90 /* Assets.xcassets */,
254 | 407D7CF127024EBE00D2BA90 /* Preview Content */,
255 | );
256 | path = CoreDataCloudKitShare;
257 | sourceTree = "";
258 | };
259 | 407D7CF127024EBE00D2BA90 /* Preview Content */ = {
260 | isa = PBXGroup;
261 | children = (
262 | 407D7CF227024EBE00D2BA90 /* Preview Assets.xcassets */,
263 | );
264 | path = "Preview Content";
265 | sourceTree = "";
266 | };
267 | 407D7CFF27024F5D00D2BA90 /* Persistence */ = {
268 | isa = PBXGroup;
269 | children = (
270 | 40AE53F02719F98C00B978CA /* CoreDataHelper.swift */,
271 | 407D7D0027024F5D00D2BA90 /* PersistenceController.swift */,
272 | 407D7D5E2703878000D2BA90 /* PersistenceController+Photo.swift */,
273 | 407D7D602703880E00D2BA90 /* PersistenceController+Tag.swift */,
274 | 40527E18270BC8FE00DB2901 /* PersistenceController+Rating.swift */,
275 | 40BF672C273B17A200ED9D2D /* PersistenceController+History.swift */,
276 | 40D68E932719101A00FB9B78 /* PersistenceController+Deduplicate.swift */,
277 | 4074428A270FB2B5009CABC7 /* PersistenceController+Share.swift */,
278 | 407D7CF627024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodeld */,
279 | );
280 | path = Persistence;
281 | sourceTree = "";
282 | };
283 | 407D7D082702502400D2BA90 /* Frameworks */ = {
284 | isa = PBXGroup;
285 | children = (
286 | 407D7D38270262EF00D2BA90 /* CloudKit.framework */,
287 | 407D7D092702502400D2BA90 /* CloudKit.framework */,
288 | );
289 | name = Frameworks;
290 | sourceTree = "";
291 | };
292 | 407D7D0E2702556600D2BA90 /* CoreDataCloudKitShareOnWatch */ = {
293 | isa = PBXGroup;
294 | children = (
295 | 406193C22755686C006EC5D8 /* Info.plist */,
296 | 407D7D0F2702556700D2BA90 /* Assets.xcassets */,
297 | );
298 | path = CoreDataCloudKitShareOnWatch;
299 | sourceTree = "";
300 | };
301 | 407D7D192702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension */ = {
302 | isa = PBXGroup;
303 | children = (
304 | 407D7D37270262ED00D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension.entitlements */,
305 | 407D7D1A2702556800D2BA90 /* CoreDataCloudKitShareApp.swift */,
306 | 401C06EC276E9D790024D92C /* ExtensionDelegate.swift */,
307 | 407D7D242702556900D2BA90 /* Assets.xcassets */,
308 | 407D7D292702556900D2BA90 /* Info.plist */,
309 | 407D7D262702556900D2BA90 /* Preview Content */,
310 | );
311 | path = "CoreDataCloudKitShareOnWatch WatchKit Extension";
312 | sourceTree = "";
313 | };
314 | 407D7D262702556900D2BA90 /* Preview Content */ = {
315 | isa = PBXGroup;
316 | children = (
317 | 407D7D272702556900D2BA90 /* Preview Assets.xcassets */,
318 | );
319 | path = "Preview Content";
320 | sourceTree = "";
321 | };
322 | /* End PBXGroup section */
323 |
324 | /* Begin PBXNativeTarget section */
325 | 407A0CD92707D0CD00F481C5 /* InitializeCloudKitSchema */ = {
326 | isa = PBXNativeTarget;
327 | buildConfigurationList = 407A0CF32707D0CD00F481C5 /* Build configuration list for PBXNativeTarget "InitializeCloudKitSchema" */;
328 | buildPhases = (
329 | 407A0CDC2707D0CD00F481C5 /* Sources */,
330 | 407A0CEC2707D0CD00F481C5 /* Frameworks */,
331 | 407A0CEE2707D0CD00F481C5 /* Resources */,
332 | 407A0CF12707D0CD00F481C5 /* Embed Watch Content */,
333 | F285BB193DC48B9B86804FEF /* Winnow */,
334 | );
335 | buildRules = (
336 | );
337 | dependencies = (
338 | );
339 | name = InitializeCloudKitSchema;
340 | productName = CoreDataCloudKitShare;
341 | productReference = 407A0CF62707D0CD00F481C5 /* InitializeCloudKitSchema.app */;
342 | productType = "com.apple.product-type.application";
343 | };
344 | 407D7CE727024EBD00D2BA90 /* CoreDataCloudKitShare */ = {
345 | isa = PBXNativeTarget;
346 | buildConfigurationList = 407D7CFB27024EBE00D2BA90 /* Build configuration list for PBXNativeTarget "CoreDataCloudKitShare" */;
347 | buildPhases = (
348 | 407D7CE427024EBD00D2BA90 /* Sources */,
349 | 407D7CE527024EBD00D2BA90 /* Frameworks */,
350 | 407D7CE627024EBD00D2BA90 /* Resources */,
351 | 407D7D2E2702556900D2BA90 /* Embed Watch Content */,
352 | 69D6E6F747E210AF1CB2FC19 /* Winnow */,
353 | );
354 | buildRules = (
355 | );
356 | dependencies = (
357 | 407D7D2C2702556900D2BA90 /* PBXTargetDependency */,
358 | );
359 | name = CoreDataCloudKitShare;
360 | productName = CoreDataCloudKitShare;
361 | productReference = 407D7CE827024EBD00D2BA90 /* CoreDataCloudKitShare.app */;
362 | productType = "com.apple.product-type.application";
363 | };
364 | 407D7D0C2702556600D2BA90 /* CoreDataCloudKitShareOnWatch */ = {
365 | isa = PBXNativeTarget;
366 | buildConfigurationList = 407D7D352702556900D2BA90 /* Build configuration list for PBXNativeTarget "CoreDataCloudKitShareOnWatch" */;
367 | buildPhases = (
368 | 407D7D0B2702556600D2BA90 /* Resources */,
369 | 407D7D312702556900D2BA90 /* Embed App Extensions */,
370 | 18882F01A9DBF0DA5A276833 /* Winnow */,
371 | );
372 | buildRules = (
373 | );
374 | dependencies = (
375 | 407D7D182702556800D2BA90 /* PBXTargetDependency */,
376 | );
377 | name = CoreDataCloudKitShareOnWatch;
378 | productName = CoreDataCloudKitShareOnWatch;
379 | productReference = 407D7D0D2702556600D2BA90 /* CoreDataCloudKitShareOnWatch.app */;
380 | productType = "com.apple.product-type.application.watchapp2";
381 | };
382 | 407D7D142702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension */ = {
383 | isa = PBXNativeTarget;
384 | buildConfigurationList = 407D7D342702556900D2BA90 /* Build configuration list for PBXNativeTarget "CoreDataCloudKitShareOnWatch WatchKit Extension" */;
385 | buildPhases = (
386 | 407D7D112702556800D2BA90 /* Sources */,
387 | 407D7D122702556800D2BA90 /* Frameworks */,
388 | 407D7D132702556800D2BA90 /* Resources */,
389 | 3215D275E631E661EDB54362 /* Winnow */,
390 | );
391 | buildRules = (
392 | );
393 | dependencies = (
394 | );
395 | name = "CoreDataCloudKitShareOnWatch WatchKit Extension";
396 | productName = "CoreDataCloudKitShareOnWatch WatchKit Extension";
397 | productReference = 407D7D152702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension.appex */;
398 | productType = "com.apple.product-type.watchkit2-extension";
399 | };
400 | /* End PBXNativeTarget section */
401 |
402 | /* Begin PBXProject section */
403 | 407D7CE027024EBD00D2BA90 /* Project object */ = {
404 | isa = PBXProject;
405 | attributes = {
406 | BuildIndependentTargetsInParallel = 1;
407 | LastSwiftUpdateCheck = 1300;
408 | LastUpgradeCheck = 1300;
409 | TargetAttributes = {
410 | 407D7CE727024EBD00D2BA90 = {
411 | CreatedOnToolsVersion = 13.0;
412 | };
413 | 407D7D0C2702556600D2BA90 = {
414 | CreatedOnToolsVersion = 13.0;
415 | };
416 | 407D7D142702556800D2BA90 = {
417 | CreatedOnToolsVersion = 13.0;
418 | };
419 | };
420 | };
421 | buildConfigurationList = 407D7CE327024EBD00D2BA90 /* Build configuration list for PBXProject "CoreDataCloudKitShare" */;
422 | compatibilityVersion = "Xcode 13.0";
423 | developmentRegion = en;
424 | hasScannedForEncodings = 0;
425 | knownRegions = (
426 | en,
427 | Base,
428 | );
429 | mainGroup = 407D7CDF27024EBD00D2BA90;
430 | productRefGroup = 407D7CE927024EBD00D2BA90 /* Products */;
431 | projectDirPath = "";
432 | projectRoot = "";
433 | targets = (
434 | 407D7CE727024EBD00D2BA90 /* CoreDataCloudKitShare */,
435 | 407A0CD92707D0CD00F481C5 /* InitializeCloudKitSchema */,
436 | 407D7D0C2702556600D2BA90 /* CoreDataCloudKitShareOnWatch */,
437 | 407D7D142702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension */,
438 | );
439 | };
440 | /* End PBXProject section */
441 |
442 | /* Begin PBXResourcesBuildPhase section */
443 | 407A0CEE2707D0CD00F481C5 /* Resources */ = {
444 | isa = PBXResourcesBuildPhase;
445 | buildActionMask = 2147483647;
446 | files = (
447 | 407A0CEF2707D0CD00F481C5 /* Preview Assets.xcassets in Resources */,
448 | 407A0CF02707D0CD00F481C5 /* Assets.xcassets in Resources */,
449 | );
450 | runOnlyForDeploymentPostprocessing = 0;
451 | };
452 | 407D7CE627024EBD00D2BA90 /* Resources */ = {
453 | isa = PBXResourcesBuildPhase;
454 | buildActionMask = 2147483647;
455 | files = (
456 | 407D7CF327024EBE00D2BA90 /* Preview Assets.xcassets in Resources */,
457 | 407D7CF027024EBE00D2BA90 /* Assets.xcassets in Resources */,
458 | );
459 | runOnlyForDeploymentPostprocessing = 0;
460 | };
461 | 407D7D0B2702556600D2BA90 /* Resources */ = {
462 | isa = PBXResourcesBuildPhase;
463 | buildActionMask = 2147483647;
464 | files = (
465 | 407D7D102702556700D2BA90 /* Assets.xcassets in Resources */,
466 | );
467 | runOnlyForDeploymentPostprocessing = 0;
468 | };
469 | 407D7D132702556800D2BA90 /* Resources */ = {
470 | isa = PBXResourcesBuildPhase;
471 | buildActionMask = 2147483647;
472 | files = (
473 | 407D7D282702556900D2BA90 /* Preview Assets.xcassets in Resources */,
474 | 407D7D252702556900D2BA90 /* Assets.xcassets in Resources */,
475 | );
476 | runOnlyForDeploymentPostprocessing = 0;
477 | };
478 | /* End PBXResourcesBuildPhase section */
479 |
480 | /* Begin PBXShellScriptBuildPhase section */
481 | 18882F01A9DBF0DA5A276833 /* Winnow */ = {
482 | isa = PBXShellScriptBuildPhase;
483 | buildActionMask = 2147483647;
484 | files = (
485 | );
486 | inputPaths = (
487 | );
488 | name = Winnow;
489 | outputPaths = (
490 | );
491 | runOnlyForDeploymentPostprocessing = 0;
492 | shellPath = /bin/sh;
493 | shellScript = "#$(dirname \"$(which winnow)\")/Winnow.app/Contents/Resources/winnow_run_script.sh\n";
494 | };
495 | 3215D275E631E661EDB54362 /* Winnow */ = {
496 | isa = PBXShellScriptBuildPhase;
497 | buildActionMask = 2147483647;
498 | files = (
499 | );
500 | inputPaths = (
501 | );
502 | name = Winnow;
503 | outputPaths = (
504 | );
505 | runOnlyForDeploymentPostprocessing = 0;
506 | shellPath = /bin/sh;
507 | shellScript = "#$(dirname \"$(which winnow)\")/Winnow.app/Contents/Resources/winnow_run_script.sh\n";
508 | };
509 | 69D6E6F747E210AF1CB2FC19 /* Winnow */ = {
510 | isa = PBXShellScriptBuildPhase;
511 | buildActionMask = 2147483647;
512 | files = (
513 | );
514 | inputPaths = (
515 | );
516 | name = Winnow;
517 | outputPaths = (
518 | );
519 | runOnlyForDeploymentPostprocessing = 0;
520 | shellPath = /bin/sh;
521 | shellScript = "#$(dirname \"$(which winnow)\")/Winnow.app/Contents/Resources/winnow_run_script.sh\n";
522 | };
523 | F285BB193DC48B9B86804FEF /* Winnow */ = {
524 | isa = PBXShellScriptBuildPhase;
525 | buildActionMask = 2147483647;
526 | files = (
527 | );
528 | inputPaths = (
529 | );
530 | name = Winnow;
531 | outputPaths = (
532 | );
533 | runOnlyForDeploymentPostprocessing = 0;
534 | shellPath = /bin/sh;
535 | shellScript = "#$(dirname \"$(which winnow)\")/Winnow.app/Contents/Resources/winnow_run_script.sh\n";
536 | };
537 | /* End PBXShellScriptBuildPhase section */
538 |
539 | /* Begin PBXSourcesBuildPhase section */
540 | 407A0CDC2707D0CD00F481C5 /* Sources */ = {
541 | isa = PBXSourcesBuildPhase;
542 | buildActionMask = 2147483647;
543 | files = (
544 | 407A0CDE2707D0CD00F481C5 /* SceneDelegate.swift in Sources */,
545 | 407A0CE02707D0CD00F481C5 /* AppDelegate.swift in Sources */,
546 | 407A0CE12707D0CD00F481C5 /* PersistenceController.swift in Sources */,
547 | 407A0CEA2707D0CD00F481C5 /* CoreDataCloudKitShare.xcdatamodeld in Sources */,
548 | 40BF6728273B0B7400ED9D2D /* CoreDataCloudKitShareApp.swift in Sources */,
549 | );
550 | runOnlyForDeploymentPostprocessing = 0;
551 | };
552 | 407D7CE427024EBD00D2BA90 /* Sources */ = {
553 | isa = PBXSourcesBuildPhase;
554 | buildActionMask = 2147483647;
555 | files = (
556 | 407D7D6B2704334400D2BA90 /* SceneDelegate.swift in Sources */,
557 | 40AE53F12719F98C00B978CA /* CoreDataHelper.swift in Sources */,
558 | 407D7D6D2704335A00D2BA90 /* AppDelegate.swift in Sources */,
559 | 40BF672F273B17A900ED9D2D /* PersistenceController+History.swift in Sources */,
560 | 408BD4DA2713B29900294A81 /* AddToExistingShareView.swift in Sources */,
561 | 4058316A2767DCDE0044E86D /* FullImageView.swift in Sources */,
562 | 4035B60E27150DA100F46D6B /* PhotoContextMenu.swift in Sources */,
563 | 4074428B270FB2B5009CABC7 /* PersistenceController+Share.swift in Sources */,
564 | 40D68E942719101A00FB9B78 /* PersistenceController+Deduplicate.swift in Sources */,
565 | 407D7D0427024F5D00D2BA90 /* PersistenceController.swift in Sources */,
566 | 407D7D3B2702A2A400D2BA90 /* PhotoPicker.swift in Sources */,
567 | 40744289270FAFB3009CABC7 /* ParticipantView.swift in Sources */,
568 | 403177D82742F4F70048F2DC /* SwiftUIHelper.swift in Sources */,
569 | 407D7CEE27024EBD00D2BA90 /* PhotoGridView.swift in Sources */,
570 | 408BD4DC2713F0F400294A81 /* ManagingSharesView.swift in Sources */,
571 | 40527E19270BC8FE00DB2901 /* PersistenceController+Rating.swift in Sources */,
572 | 407D7D612703880E00D2BA90 /* PersistenceController+Tag.swift in Sources */,
573 | 407D7CEC27024EBD00D2BA90 /* CoreDataCloudKitShareApp.swift in Sources */,
574 | 40527E17270BBE3400DB2901 /* RatingView.swift in Sources */,
575 | 407D7D5F2703878000D2BA90 /* PersistenceController+Photo.swift in Sources */,
576 | 407D7CF827024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodeld in Sources */,
577 | 407D7D632703A27000D2BA90 /* PhotoGridItemView.swift in Sources */,
578 | 40C6211E2755AFB500015301 /* SharePickerView.swift in Sources */,
579 | 4026B868270A19390060F0F7 /* TaggingView.swift in Sources */,
580 | );
581 | runOnlyForDeploymentPostprocessing = 0;
582 | };
583 | 407D7D112702556800D2BA90 /* Sources */ = {
584 | isa = PBXSourcesBuildPhase;
585 | buildActionMask = 2147483647;
586 | files = (
587 | 40B6201F273AFD3400B27D3D /* CoreDataHelper.swift in Sources */,
588 | 40B62020273AFD3700B27D3D /* PersistenceController.swift in Sources */,
589 | 40BF672E273B17A500ED9D2D /* PersistenceController+History.swift in Sources */,
590 | 403177DB27430E340048F2DC /* ParticipantView.swift in Sources */,
591 | 403177DA27430A800048F2DC /* PhotoGridView.swift in Sources */,
592 | 403177DD274310B50048F2DC /* ManagingSharesView.swift in Sources */,
593 | 40B62024273AFD9900B27D3D /* PersistenceController+Tag.swift in Sources */,
594 | 40B62021273AFD6700B27D3D /* CoreDataCloudKitShare.xcdatamodeld in Sources */,
595 | 403177DC274310690048F2DC /* AddToExistingShareView.swift in Sources */,
596 | 40B62027273AFDC600B27D3D /* PersistenceController+Photo.swift in Sources */,
597 | 403177DE27432CE40048F2DC /* PhotoContextMenu.swift in Sources */,
598 | 401C06ED276E9D790024D92C /* ExtensionDelegate.swift in Sources */,
599 | 403177D92742F4F70048F2DC /* SwiftUIHelper.swift in Sources */,
600 | 403177D42742DB270048F2DC /* TaggingView.swift in Sources */,
601 | 40B62022273AFD9300B27D3D /* PersistenceController+Deduplicate.swift in Sources */,
602 | 4058316B2767F0ED0044E86D /* FullImageView.swift in Sources */,
603 | 407D7D1B2702556800D2BA90 /* CoreDataCloudKitShareApp.swift in Sources */,
604 | 40BACC0B2740646E00F12CBD /* PersistenceController+Share.swift in Sources */,
605 | 40C6211F2755AFB500015301 /* SharePickerView.swift in Sources */,
606 | 403177D62742F4B40048F2DC /* PhotoGridItemView.swift in Sources */,
607 | 40B62023273AFD9700B27D3D /* PersistenceController+Rating.swift in Sources */,
608 | 403177D32742D5790048F2DC /* RatingView.swift in Sources */,
609 | );
610 | runOnlyForDeploymentPostprocessing = 0;
611 | };
612 | /* End PBXSourcesBuildPhase section */
613 |
614 | /* Begin PBXTargetDependency section */
615 | 407D7D182702556800D2BA90 /* PBXTargetDependency */ = {
616 | isa = PBXTargetDependency;
617 | target = 407D7D142702556800D2BA90 /* CoreDataCloudKitShareOnWatch WatchKit Extension */;
618 | targetProxy = 407D7D172702556800D2BA90 /* PBXContainerItemProxy */;
619 | };
620 | 407D7D2C2702556900D2BA90 /* PBXTargetDependency */ = {
621 | isa = PBXTargetDependency;
622 | target = 407D7D0C2702556600D2BA90 /* CoreDataCloudKitShareOnWatch */;
623 | targetProxy = 407D7D2B2702556900D2BA90 /* PBXContainerItemProxy */;
624 | };
625 | /* End PBXTargetDependency section */
626 |
627 | /* Begin XCBuildConfiguration section */
628 | 407A0CF42707D0CD00F481C5 /* Debug */ = {
629 | isa = XCBuildConfiguration;
630 | buildSettings = {
631 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
632 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
633 | CODE_SIGN_ENTITLEMENTS = CoreDataCloudKitShare/CoreDataCloudKitShare.entitlements;
634 | CODE_SIGN_STYLE = Automatic;
635 | CURRENT_PROJECT_VERSION = 1;
636 | DEVELOPMENT_ASSET_PATHS = "\"CoreDataCloudKitShare/Preview Content\"";
637 | DEVELOPMENT_TEAM = "";
638 | ENABLE_PREVIEWS = YES;
639 | GENERATE_INFOPLIST_FILE = YES;
640 | INFOPLIST_FILE = "CoreDataCloudKitShare/InitializeCloudKitSchema-Info.plist";
641 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
642 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
643 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
644 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
645 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
646 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
647 | LD_RUNPATH_SEARCH_PATHS = (
648 | "$(inherited)",
649 | "@executable_path/Frameworks",
650 | );
651 | MARKETING_VERSION = 1.0;
652 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.ziqiao-samplecode.CoreDataCloudKitShare";
653 | PRODUCT_NAME = "$(TARGET_NAME)";
654 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG InitializeCloudKitSchema";
655 | SWIFT_EMIT_LOC_STRINGS = YES;
656 | SWIFT_VERSION = 5.0;
657 | TARGETED_DEVICE_FAMILY = "1,2";
658 | };
659 | name = Debug;
660 | };
661 | 407A0CF52707D0CD00F481C5 /* Release */ = {
662 | isa = XCBuildConfiguration;
663 | buildSettings = {
664 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
665 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
666 | CODE_SIGN_ENTITLEMENTS = CoreDataCloudKitShare/CoreDataCloudKitShare.entitlements;
667 | CODE_SIGN_STYLE = Automatic;
668 | CURRENT_PROJECT_VERSION = 1;
669 | DEVELOPMENT_ASSET_PATHS = "\"CoreDataCloudKitShare/Preview Content\"";
670 | DEVELOPMENT_TEAM = "";
671 | ENABLE_PREVIEWS = YES;
672 | GENERATE_INFOPLIST_FILE = YES;
673 | INFOPLIST_FILE = "CoreDataCloudKitShare/InitializeCloudKitSchema-Info.plist";
674 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
675 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
676 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
677 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
678 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
679 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
680 | LD_RUNPATH_SEARCH_PATHS = (
681 | "$(inherited)",
682 | "@executable_path/Frameworks",
683 | );
684 | MARKETING_VERSION = 1.0;
685 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.ziqiao-samplecode.CoreDataCloudKitShare";
686 | PRODUCT_NAME = "$(TARGET_NAME)";
687 | SWIFT_EMIT_LOC_STRINGS = YES;
688 | SWIFT_VERSION = 5.0;
689 | TARGETED_DEVICE_FAMILY = "1,2";
690 | };
691 | name = Release;
692 | };
693 | 407D7CF927024EBE00D2BA90 /* Debug */ = {
694 | isa = XCBuildConfiguration;
695 | buildSettings = {
696 | ALWAYS_SEARCH_USER_PATHS = NO;
697 | CLANG_ANALYZER_NONNULL = YES;
698 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
699 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
700 | CLANG_CXX_LIBRARY = "libc++";
701 | CLANG_ENABLE_MODULES = YES;
702 | CLANG_ENABLE_OBJC_ARC = YES;
703 | CLANG_ENABLE_OBJC_WEAK = YES;
704 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
705 | CLANG_WARN_BOOL_CONVERSION = YES;
706 | CLANG_WARN_COMMA = YES;
707 | CLANG_WARN_CONSTANT_CONVERSION = YES;
708 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
709 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
710 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
711 | CLANG_WARN_EMPTY_BODY = YES;
712 | CLANG_WARN_ENUM_CONVERSION = YES;
713 | CLANG_WARN_INFINITE_RECURSION = YES;
714 | CLANG_WARN_INT_CONVERSION = YES;
715 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
716 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
717 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
718 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
719 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
720 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
721 | CLANG_WARN_STRICT_PROTOTYPES = YES;
722 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
723 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
724 | CLANG_WARN_UNREACHABLE_CODE = YES;
725 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
726 | COPY_PHASE_STRIP = NO;
727 | DEBUG_INFORMATION_FORMAT = dwarf;
728 | ENABLE_STRICT_OBJC_MSGSEND = YES;
729 | ENABLE_TESTABILITY = YES;
730 | GCC_C_LANGUAGE_STANDARD = gnu11;
731 | GCC_DYNAMIC_NO_PIC = NO;
732 | GCC_NO_COMMON_BLOCKS = YES;
733 | GCC_OPTIMIZATION_LEVEL = 0;
734 | GCC_PREPROCESSOR_DEFINITIONS = (
735 | "DEBUG=1",
736 | "$(inherited)",
737 | );
738 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
739 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
740 | GCC_WARN_UNDECLARED_SELECTOR = YES;
741 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
742 | GCC_WARN_UNUSED_FUNCTION = YES;
743 | GCC_WARN_UNUSED_VARIABLE = YES;
744 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
745 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
746 | MTL_FAST_MATH = YES;
747 | ONLY_ACTIVE_ARCH = YES;
748 | SDKROOT = iphoneos;
749 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
750 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
751 | };
752 | name = Debug;
753 | };
754 | 407D7CFA27024EBE00D2BA90 /* Release */ = {
755 | isa = XCBuildConfiguration;
756 | buildSettings = {
757 | ALWAYS_SEARCH_USER_PATHS = NO;
758 | CLANG_ANALYZER_NONNULL = YES;
759 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
760 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
761 | CLANG_CXX_LIBRARY = "libc++";
762 | CLANG_ENABLE_MODULES = YES;
763 | CLANG_ENABLE_OBJC_ARC = YES;
764 | CLANG_ENABLE_OBJC_WEAK = YES;
765 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
766 | CLANG_WARN_BOOL_CONVERSION = YES;
767 | CLANG_WARN_COMMA = YES;
768 | CLANG_WARN_CONSTANT_CONVERSION = YES;
769 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
770 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
771 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
772 | CLANG_WARN_EMPTY_BODY = YES;
773 | CLANG_WARN_ENUM_CONVERSION = YES;
774 | CLANG_WARN_INFINITE_RECURSION = YES;
775 | CLANG_WARN_INT_CONVERSION = YES;
776 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
777 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
778 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
779 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
780 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
781 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
782 | CLANG_WARN_STRICT_PROTOTYPES = YES;
783 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
784 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
785 | CLANG_WARN_UNREACHABLE_CODE = YES;
786 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
787 | COPY_PHASE_STRIP = NO;
788 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
789 | ENABLE_NS_ASSERTIONS = NO;
790 | ENABLE_STRICT_OBJC_MSGSEND = YES;
791 | GCC_C_LANGUAGE_STANDARD = gnu11;
792 | GCC_NO_COMMON_BLOCKS = YES;
793 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
794 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
795 | GCC_WARN_UNDECLARED_SELECTOR = YES;
796 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
797 | GCC_WARN_UNUSED_FUNCTION = YES;
798 | GCC_WARN_UNUSED_VARIABLE = YES;
799 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
800 | MTL_ENABLE_DEBUG_INFO = NO;
801 | MTL_FAST_MATH = YES;
802 | SDKROOT = iphoneos;
803 | SWIFT_COMPILATION_MODE = wholemodule;
804 | SWIFT_OPTIMIZATION_LEVEL = "-O";
805 | VALIDATE_PRODUCT = YES;
806 | };
807 | name = Release;
808 | };
809 | 407D7CFC27024EBE00D2BA90 /* Debug */ = {
810 | isa = XCBuildConfiguration;
811 | buildSettings = {
812 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
813 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
814 | CODE_SIGN_ENTITLEMENTS = CoreDataCloudKitShare/CoreDataCloudKitShare.entitlements;
815 | CODE_SIGN_STYLE = Automatic;
816 | CURRENT_PROJECT_VERSION = 1;
817 | DEVELOPMENT_ASSET_PATHS = "\"CoreDataCloudKitShare/Preview Content\"";
818 | DEVELOPMENT_TEAM = "";
819 | ENABLE_PREVIEWS = YES;
820 | GENERATE_INFOPLIST_FILE = YES;
821 | INFOPLIST_FILE = CoreDataCloudKitShare/Info.plist;
822 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
823 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
824 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
825 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
826 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
827 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
828 | LD_RUNPATH_SEARCH_PATHS = (
829 | "$(inherited)",
830 | "@executable_path/Frameworks",
831 | );
832 | MARKETING_VERSION = 1.0;
833 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.ziqiao-samplecode.CoreDataCloudKitShare";
834 | PRODUCT_NAME = "$(TARGET_NAME)";
835 | SWIFT_EMIT_LOC_STRINGS = YES;
836 | SWIFT_VERSION = 5.0;
837 | TARGETED_DEVICE_FAMILY = "1,2";
838 | };
839 | name = Debug;
840 | };
841 | 407D7CFD27024EBE00D2BA90 /* Release */ = {
842 | isa = XCBuildConfiguration;
843 | buildSettings = {
844 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
845 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
846 | CODE_SIGN_ENTITLEMENTS = CoreDataCloudKitShare/CoreDataCloudKitShare.entitlements;
847 | CODE_SIGN_STYLE = Automatic;
848 | CURRENT_PROJECT_VERSION = 1;
849 | DEVELOPMENT_ASSET_PATHS = "\"CoreDataCloudKitShare/Preview Content\"";
850 | DEVELOPMENT_TEAM = "";
851 | ENABLE_PREVIEWS = YES;
852 | GENERATE_INFOPLIST_FILE = YES;
853 | INFOPLIST_FILE = CoreDataCloudKitShare/Info.plist;
854 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
855 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
856 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
857 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
858 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
859 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
860 | LD_RUNPATH_SEARCH_PATHS = (
861 | "$(inherited)",
862 | "@executable_path/Frameworks",
863 | );
864 | MARKETING_VERSION = 1.0;
865 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.ziqiao-samplecode.CoreDataCloudKitShare";
866 | PRODUCT_NAME = "$(TARGET_NAME)";
867 | SWIFT_EMIT_LOC_STRINGS = YES;
868 | SWIFT_VERSION = 5.0;
869 | TARGETED_DEVICE_FAMILY = "1,2";
870 | };
871 | name = Release;
872 | };
873 | 407D7D2F2702556900D2BA90 /* Debug */ = {
874 | isa = XCBuildConfiguration;
875 | buildSettings = {
876 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
877 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
878 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
879 | CODE_SIGN_STYLE = Automatic;
880 | CURRENT_PROJECT_VERSION = 1;
881 | DEVELOPMENT_TEAM = "";
882 | GENERATE_INFOPLIST_FILE = YES;
883 | IBSC_MODULE = CoreDataCloudKitShareOnWatch_WatchKit_Extension;
884 | INFOPLIST_FILE = CoreDataCloudKitShareOnWatch/Info.plist;
885 | INFOPLIST_KEY_CFBundleDisplayName = CoreDataCloudKitShareOnWatch;
886 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
887 | INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "com.example.ziqiao-samplecode.CoreDataCloudKitShare";
888 | MARKETING_VERSION = 1.0;
889 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.ziqiao-samplecode.CoreDataCloudKitShare.watchkitapp";
890 | PRODUCT_NAME = "$(TARGET_NAME)";
891 | SDKROOT = watchos;
892 | SKIP_INSTALL = YES;
893 | SWIFT_EMIT_LOC_STRINGS = YES;
894 | SWIFT_VERSION = 5.0;
895 | TARGETED_DEVICE_FAMILY = 4;
896 | WATCHOS_DEPLOYMENT_TARGET = 8.0;
897 | };
898 | name = Debug;
899 | };
900 | 407D7D302702556900D2BA90 /* Release */ = {
901 | isa = XCBuildConfiguration;
902 | buildSettings = {
903 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
904 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
905 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
906 | CODE_SIGN_STYLE = Automatic;
907 | CURRENT_PROJECT_VERSION = 1;
908 | DEVELOPMENT_TEAM = "";
909 | GENERATE_INFOPLIST_FILE = YES;
910 | IBSC_MODULE = CoreDataCloudKitShareOnWatch_WatchKit_Extension;
911 | INFOPLIST_FILE = CoreDataCloudKitShareOnWatch/Info.plist;
912 | INFOPLIST_KEY_CFBundleDisplayName = CoreDataCloudKitShareOnWatch;
913 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
914 | INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "com.example.ziqiao-samplecode.CoreDataCloudKitShare";
915 | MARKETING_VERSION = 1.0;
916 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.ziqiao-samplecode.CoreDataCloudKitShare.watchkitapp";
917 | PRODUCT_NAME = "$(TARGET_NAME)";
918 | SDKROOT = watchos;
919 | SKIP_INSTALL = YES;
920 | SWIFT_EMIT_LOC_STRINGS = YES;
921 | SWIFT_VERSION = 5.0;
922 | TARGETED_DEVICE_FAMILY = 4;
923 | WATCHOS_DEPLOYMENT_TARGET = 8.0;
924 | };
925 | name = Release;
926 | };
927 | 407D7D322702556900D2BA90 /* Debug */ = {
928 | isa = XCBuildConfiguration;
929 | buildSettings = {
930 | ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
931 | CODE_SIGN_ENTITLEMENTS = "CoreDataCloudKitShareOnWatch WatchKit Extension/CoreDataCloudKitShareOnWatch WatchKit Extension.entitlements";
932 | CODE_SIGN_STYLE = Automatic;
933 | CURRENT_PROJECT_VERSION = 1;
934 | DEVELOPMENT_ASSET_PATHS = "\"CoreDataCloudKitShareOnWatch WatchKit Extension/Preview Content\"";
935 | DEVELOPMENT_TEAM = "";
936 | ENABLE_PREVIEWS = YES;
937 | GENERATE_INFOPLIST_FILE = YES;
938 | INFOPLIST_FILE = "CoreDataCloudKitShareOnWatch WatchKit Extension/Info.plist";
939 | INFOPLIST_KEY_CFBundleDisplayName = "CoreDataCloudKitShareOnWatch WatchKit Extension";
940 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
941 | LD_RUNPATH_SEARCH_PATHS = (
942 | "$(inherited)",
943 | "@executable_path/Frameworks",
944 | "@executable_path/../../Frameworks",
945 | );
946 | MARKETING_VERSION = 1.0;
947 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.ziqiao-samplecode.CoreDataCloudKitShare.watchkitapp.watchkitextension";
948 | PRODUCT_NAME = "${TARGET_NAME}";
949 | SDKROOT = watchos;
950 | SKIP_INSTALL = YES;
951 | SWIFT_EMIT_LOC_STRINGS = YES;
952 | SWIFT_VERSION = 5.0;
953 | TARGETED_DEVICE_FAMILY = 4;
954 | WATCHOS_DEPLOYMENT_TARGET = 8.0;
955 | };
956 | name = Debug;
957 | };
958 | 407D7D332702556900D2BA90 /* Release */ = {
959 | isa = XCBuildConfiguration;
960 | buildSettings = {
961 | ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
962 | CODE_SIGN_ENTITLEMENTS = "CoreDataCloudKitShareOnWatch WatchKit Extension/CoreDataCloudKitShareOnWatch WatchKit Extension.entitlements";
963 | CODE_SIGN_STYLE = Automatic;
964 | CURRENT_PROJECT_VERSION = 1;
965 | DEVELOPMENT_ASSET_PATHS = "\"CoreDataCloudKitShareOnWatch WatchKit Extension/Preview Content\"";
966 | DEVELOPMENT_TEAM = "";
967 | ENABLE_PREVIEWS = YES;
968 | GENERATE_INFOPLIST_FILE = YES;
969 | INFOPLIST_FILE = "CoreDataCloudKitShareOnWatch WatchKit Extension/Info.plist";
970 | INFOPLIST_KEY_CFBundleDisplayName = "CoreDataCloudKitShareOnWatch WatchKit Extension";
971 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
972 | LD_RUNPATH_SEARCH_PATHS = (
973 | "$(inherited)",
974 | "@executable_path/Frameworks",
975 | "@executable_path/../../Frameworks",
976 | );
977 | MARKETING_VERSION = 1.0;
978 | PRODUCT_BUNDLE_IDENTIFIER = "com.example.ziqiao-samplecode.CoreDataCloudKitShare.watchkitapp.watchkitextension";
979 | PRODUCT_NAME = "${TARGET_NAME}";
980 | SDKROOT = watchos;
981 | SKIP_INSTALL = YES;
982 | SWIFT_EMIT_LOC_STRINGS = YES;
983 | SWIFT_VERSION = 5.0;
984 | TARGETED_DEVICE_FAMILY = 4;
985 | WATCHOS_DEPLOYMENT_TARGET = 8.0;
986 | };
987 | name = Release;
988 | };
989 | /* End XCBuildConfiguration section */
990 |
991 | /* Begin XCConfigurationList section */
992 | 407A0CF32707D0CD00F481C5 /* Build configuration list for PBXNativeTarget "InitializeCloudKitSchema" */ = {
993 | isa = XCConfigurationList;
994 | buildConfigurations = (
995 | 407A0CF42707D0CD00F481C5 /* Debug */,
996 | 407A0CF52707D0CD00F481C5 /* Release */,
997 | );
998 | defaultConfigurationIsVisible = 0;
999 | defaultConfigurationName = Release;
1000 | };
1001 | 407D7CE327024EBD00D2BA90 /* Build configuration list for PBXProject "CoreDataCloudKitShare" */ = {
1002 | isa = XCConfigurationList;
1003 | buildConfigurations = (
1004 | 407D7CF927024EBE00D2BA90 /* Debug */,
1005 | 407D7CFA27024EBE00D2BA90 /* Release */,
1006 | );
1007 | defaultConfigurationIsVisible = 0;
1008 | defaultConfigurationName = Release;
1009 | };
1010 | 407D7CFB27024EBE00D2BA90 /* Build configuration list for PBXNativeTarget "CoreDataCloudKitShare" */ = {
1011 | isa = XCConfigurationList;
1012 | buildConfigurations = (
1013 | 407D7CFC27024EBE00D2BA90 /* Debug */,
1014 | 407D7CFD27024EBE00D2BA90 /* Release */,
1015 | );
1016 | defaultConfigurationIsVisible = 0;
1017 | defaultConfigurationName = Release;
1018 | };
1019 | 407D7D342702556900D2BA90 /* Build configuration list for PBXNativeTarget "CoreDataCloudKitShareOnWatch WatchKit Extension" */ = {
1020 | isa = XCConfigurationList;
1021 | buildConfigurations = (
1022 | 407D7D322702556900D2BA90 /* Debug */,
1023 | 407D7D332702556900D2BA90 /* Release */,
1024 | );
1025 | defaultConfigurationIsVisible = 0;
1026 | defaultConfigurationName = Release;
1027 | };
1028 | 407D7D352702556900D2BA90 /* Build configuration list for PBXNativeTarget "CoreDataCloudKitShareOnWatch" */ = {
1029 | isa = XCConfigurationList;
1030 | buildConfigurations = (
1031 | 407D7D2F2702556900D2BA90 /* Debug */,
1032 | 407D7D302702556900D2BA90 /* Release */,
1033 | );
1034 | defaultConfigurationIsVisible = 0;
1035 | defaultConfigurationName = Release;
1036 | };
1037 | /* End XCConfigurationList section */
1038 |
1039 | /* Begin XCVersionGroup section */
1040 | 407D7CF627024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodeld */ = {
1041 | isa = XCVersionGroup;
1042 | children = (
1043 | 407D7CF727024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodel */,
1044 | );
1045 | currentVersion = 407D7CF727024EBE00D2BA90 /* CoreDataCloudKitShare.xcdatamodel */;
1046 | path = CoreDataCloudKitShare.xcdatamodeld;
1047 | sourceTree = "";
1048 | versionGroupType = wrapper.xcdatamodel;
1049 | };
1050 | /* End XCVersionGroup section */
1051 | };
1052 | rootObject = 407D7CE027024EBD00D2BA90 /* Project object */;
1053 | }
1054 |
--------------------------------------------------------------------------------