diff --git a/Shared/Models/Post.swift b/Shared/Models/Post.swift index 57e1d12..9f42d9f 100644 --- a/Shared/Models/Post.swift +++ b/Shared/Models/Post.swift @@ -1,108 +1,112 @@ import Foundation import WriteFreely enum PostStatus { case local case edited case published } class Post: Identifiable, ObservableObject, Hashable { @Published var wfPost: WFPost @Published var status: PostStatus - @Published var collection: PostCollection + @Published var collection: PostCollection? @Published var hasNewerRemoteCopy: Bool = false let id = UUID() init( title: String = "Title", body: String = "Write your post here...", createdDate: Date = Date(), - status: PostStatus = .local, - collection: PostCollection = draftsCollection + status: PostStatus = .draft, + collection: PostCollection? = nil ) { self.wfPost = WFPost(body: body, title: title, createdDate: createdDate) self.status = status self.collection = collection } - convenience init(wfPost: WFPost, in collection: PostCollection = draftsCollection) { + convenience init(wfPost: WFPost, in collection: PostCollection? = nil) { self.init( title: wfPost.title ?? "", body: wfPost.body, createdDate: wfPost.createdDate ?? Date(), status: .published, collection: collection ) self.wfPost = wfPost } } extension Post { static func == (lhs: Post, rhs: Post) -> Bool { return lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } } #if DEBUG +let userCollection1 = PostCollection(title: "Collection 1") +let userCollection2 = PostCollection(title: "Collection 2") +let userCollection3 = PostCollection(title: "Collection 3") + let testPost = Post( title: "Test Post Title", body: """ Here's some cool sample body text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ultrices \ posuere dignissim. Vestibulum a libero tempor, lacinia nulla vitae, congue purus. Nunc ac nulla quam. Duis \ tincidunt eros augue, et volutpat tortor pulvinar ut. Nullam sit amet maximus urna. Phasellus non dignissim lacus.\ Nulla ac posuere ex. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec \ non molestie mauris. Suspendisse potenti. Vivamus at erat turpis. Pellentesque porttitor gravida tincidunt. Sed vitae eros non metus aliquam hendrerit. Aliquam sed risus suscipit \ turpis dictum dictum. Duis lacus lectus, dictum vel felis in, rhoncus fringilla felis. Nunc id dolor nisl. Aliquam \ euismod purus elit. Nullam egestas neque leo, sed aliquet ligula ultrices nec. """, createdDate: Date() ) let testPostData = [ Post( title: "My First Post", body: "Look at me, creating a first post! That's cool.", createdDate: Date(timeIntervalSince1970: 1595429452), status: .published, collection: userCollection1 ), Post( title: "Post 2: The Quickening", body: "See, here's the rule about Highlander jokes: _there can be only one_.", createdDate: Date(timeIntervalSince1970: 1595514125), status: .edited, collection: userCollection1 ), Post( title: "The Post Revolutions", body: "I can never keep the Matrix movie order straight. Why not just call them part 2 and part 3?", createdDate: Date(timeIntervalSince1970: 1595600006) ), Post( title: "Episode IV: A New Post", body: "How many movies does this person watch? How many movie-title jokes will they make?", createdDate: Date(timeIntervalSince1970: 1596219877), status: .published, collection: userCollection2 ), Post( title: "Fast (Post) Five", body: "Look, it was either a Fast and the Furious reference, or a Resident Evil reference." ), Post( title: "Post: The Final Chapter", body: "And there you have it, a Resident Evil movie reference.", createdDate: Date(timeIntervalSince1970: 1596043684), status: .edited, collection: userCollection3 ) ] #endif diff --git a/Shared/Models/PostCollection.swift b/Shared/Models/PostCollection.swift index 8801cff..c4a5d5b 100644 --- a/Shared/Models/PostCollection.swift +++ b/Shared/Models/PostCollection.swift @@ -1,23 +1,18 @@ import Foundation import WriteFreely -struct PostCollection: Identifiable { +class PostCollection: Identifiable { let id = UUID() - let title: String + var title: String var wfCollection: WFCollection? + + init(title: String) { + self.title = title + } } extension PostCollection { static func == (lhs: PostCollection, rhs: PostCollection) -> Bool { return lhs.id == rhs.id } } - -let allPostsCollection = PostCollection(title: "All Posts") -let draftsCollection = PostCollection(title: "Drafts") - -#if DEBUG -let userCollection1 = PostCollection(title: "Collection 1") -let userCollection2 = PostCollection(title: "Collection 2") -let userCollection3 = PostCollection(title: "Collection 3") -#endif diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index 9e6e8b8..4096de8 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -1,315 +1,327 @@ import Foundation import WriteFreely import Security // MARK: - WriteFreelyModel class WriteFreelyModel: ObservableObject { @Published var account = AccountModel() @Published var preferences = PreferencesModel() @Published var store = PostStore() - @Published var collections = CollectionListModel(with: []) + @Published var collections = CollectionListModel() @Published var isLoggingIn: Bool = false @Published var selectedPost: Post? private var client: WFClient? private let defaults = UserDefaults.standard init() { // Set the color scheme based on what's been saved in UserDefaults. DispatchQueue.main.async { self.preferences.appearance = self.defaults.integer(forKey: self.preferences.colorSchemeIntegerKey) } #if DEBUG // for post in testPostData { store.add(post) } #endif DispatchQueue.main.async { self.account.restoreState() if self.account.isLoggedIn { guard let serverURL = URL(string: self.account.server) else { print("Server URL not found") return } guard let token = self.fetchTokenFromKeychain( username: self.account.username, server: self.account.server ) else { print("Could not fetch token from Keychain") return } self.account.login(WFUser(token: token, username: self.account.username)) self.client = WFClient(for: serverURL) self.client?.user = self.account.user self.collections.clearUserCollection() self.fetchUserCollections() self.fetchUserPosts() } } } } // MARK: - WriteFreelyModel API extension WriteFreelyModel { func login(to server: URL, as username: String, password: String) { isLoggingIn = true account.server = server.absoluteString client = WFClient(for: server) client?.login(username: username, password: password, completion: loginHandler) } func logout() { guard let loggedInClient = client else { do { try purgeTokenFromKeychain(username: account.username, server: account.server) account.logout() } catch { fatalError("Failed to log out persisted state") } return } loggedInClient.logout(completion: logoutHandler) } func fetchUserCollections() { guard let loggedInClient = client else { return } loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler) } func fetchUserPosts() { guard let loggedInClient = client else { return } loggedInClient.getPosts(completion: fetchUserPostsHandler) } func publish(post: Post) { guard let loggedInClient = client else { return } if let existingPostId = post.wfPost.postId { // This is an existing post. loggedInClient.updatePost( postId: existingPostId, updatedPost: post.wfPost, completion: publishHandler ) } else { // This is a new local draft. loggedInClient.createPost( - post: post.wfPost, in: post.collection.wfCollection?.alias, completion: publishHandler + post: post.wfPost, in: post.collection?.wfCollection?.alias, completion: publishHandler ) } } func updateFromServer(post: Post) { guard let loggedInClient = client else { return } guard let postId = post.wfPost.postId else { return } DispatchQueue.main.async { self.selectedPost = post } loggedInClient.getPost(byId: postId, completion: updateFromServerHandler) } } private extension WriteFreelyModel { func loginHandler(result: Result) { DispatchQueue.main.async { self.isLoggingIn = false } do { let user = try result.get() fetchUserCollections() fetchUserPosts() saveTokenToKeychain(user.token, username: user.username, server: account.server) DispatchQueue.main.async { self.account.login(user) } } catch WFError.notFound { DispatchQueue.main.async { self.account.currentError = AccountError.usernameNotFound } } catch WFError.unauthorized { DispatchQueue.main.async { self.account.currentError = AccountError.invalidPassword } } catch { if (error as NSError).domain == NSURLErrorDomain, (error as NSError).code == -1003 { DispatchQueue.main.async { self.account.currentError = AccountError.serverNotFound } } } } func logoutHandler(result: Result) { do { _ = try result.get() do { try purgeTokenFromKeychain(username: account.user?.username, server: account.server) client = nil DispatchQueue.main.async { self.account.logout() self.collections.clearUserCollection() self.store.purgeAllPosts() } } catch { print("Something went wrong purging the token from the Keychain.") } } catch WFError.notFound { // The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with // purging the token from the Keychain, destroying the client object, and setting the AccountModel to its // logged-out state. do { try purgeTokenFromKeychain(username: account.user?.username, server: account.server) client = nil DispatchQueue.main.async { self.account.logout() self.collections.clearUserCollection() self.store.purgeAllPosts() } } catch { print("Something went wrong purging the token from the Keychain.") } } catch { // We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here, // so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're // logged in, try calling the logout function again and see what we get. // Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties. if (error as NSError).domain == NSURLErrorDomain, (error as NSError).code == NSURLErrorCannotParseResponse { if account.isLoggedIn { self.logout() } } } } func fetchUserCollectionsHandler(result: Result<[WFCollection], Error>) { do { let fetchedCollections = try result.get() var fetchedCollectionsArray: [PostCollection] = [] for fetchedCollection in fetchedCollections { - var postCollection = PostCollection(title: fetchedCollection.title) + let postCollection = PostCollection(title: fetchedCollection.title) postCollection.wfCollection = fetchedCollection fetchedCollectionsArray.append(postCollection) + + DispatchQueue.main.async { + let localCollection = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + localCollection.alias = fetchedCollection.alias + localCollection.blogDescription = fetchedCollection.description + localCollection.email = fetchedCollection.email + localCollection.isPublic = fetchedCollection.isPublic ?? false + localCollection.styleSheet = fetchedCollection.styleSheet + localCollection.title = fetchedCollection.title + localCollection.url = fetchedCollection.url + } } DispatchQueue.main.async { - self.collections = CollectionListModel(with: fetchedCollectionsArray) +// self.collections = CollectionListModel(with: fetchedCollectionsArray) + PersistenceManager().saveContext() } } catch { print(error) } } func fetchUserPostsHandler(result: Result<[WFPost], Error>) { do { let fetchedPosts = try result.get() var fetchedPostsArray: [Post] = [] for fetchedPost in fetchedPosts { var post: Post if let matchingAlias = fetchedPost.collectionAlias { - let postCollection = ( - collections.userCollections.filter { $0.wfCollection?.alias == matchingAlias } - ).first - post = Post(wfPost: fetchedPost, in: postCollection ?? draftsCollection) + let postCollection = PostCollection(title: ( + collections.userCollections.filter { $0.alias == matchingAlias } + ).first?.title ?? "NO TITLE") + post = Post(wfPost: fetchedPost, in: postCollection) } else { post = Post(wfPost: fetchedPost) } fetchedPostsArray.append(post) } DispatchQueue.main.async { self.store.updateStore(with: fetchedPostsArray) } } catch { print(error) } } func publishHandler(result: Result) { do { let wfPost = try result.get() let foundPostIndex = store.posts.firstIndex(where: { $0.wfPost.title == wfPost.title && $0.wfPost.body == wfPost.body }) guard let index = foundPostIndex else { return } DispatchQueue.main.async { self.store.posts[index].wfPost = wfPost } } catch { print(error) } } func updateFromServerHandler(result: Result) { do { let fetchedPost = try result.get() DispatchQueue.main.async { guard let selectedPost = self.selectedPost else { return } self.store.replace(post: selectedPost, with: fetchedPost) } } catch { print(error) } } } private extension WriteFreelyModel { // MARK: - Keychain Helpers func saveTokenToKeychain(_ token: String, username: String?, server: String) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecValueData as String: token.data(using: .utf8)!, kSecAttrAccount as String: username ?? "anonymous", kSecAttrService as String: server ] let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecDuplicateItem || status == errSecSuccess else { fatalError("Error storing in Keychain with OSStatus: \(status)") } } func purgeTokenFromKeychain(username: String?, server: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: username ?? "anonymous", kSecAttrService as String: server ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { fatalError("Error deleting from Keychain with OSStatus: \(status)") } } func fetchTokenFromKeychain(username: String?, server: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: username ?? "anonymous", kSecAttrService as String: server, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnAttributes as String: true, kSecReturnData as String: true ] var secItem: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &secItem) guard status != errSecItemNotFound else { return nil } guard status == errSecSuccess else { fatalError("Error fetching from Keychain with OSStatus: \(status)") } guard let existingSecItem = secItem as? [String: Any], let tokenData = existingSecItem[kSecValueData as String] as? Data, let token = String(data: tokenData, encoding: .utf8) else { return nil } return token } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index efdeef3..1213f1e 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -1,29 +1,38 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var model: WriteFreelyModel var body: some View { NavigationView { SidebarView() - PostListView(selectedCollection: allPostsCollection) + PostListView(selectedCollection: CollectionListModel.allPostsCollection) Text("Select a post, or create a new local draft.") .foregroundColor(.secondary) } .environmentObject(model) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { + let userCollection1 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + let userCollection2 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + let userCollection3 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + userCollection1.title = "Collection 1" + userCollection2.title = "Collection 2" + userCollection3.title = "Collection 3" + let model = WriteFreelyModel() - model.collections = CollectionListModel(with: [userCollection1, userCollection2, userCollection3]) + model.collections = CollectionListModel() + for post in testPostData { model.store.add(post) } return ContentView() .environmentObject(model) + .environment(\.managedObjectContext, PersistenceManager.persistentContainer.viewContext) } } diff --git a/Shared/Navigation/SidebarView.swift b/Shared/Navigation/SidebarView.swift index a95ee08..7175fa4 100644 --- a/Shared/Navigation/SidebarView.swift +++ b/Shared/Navigation/SidebarView.swift @@ -1,16 +1,25 @@ import SwiftUI struct SidebarView: View { var body: some View { CollectionListView() } } struct SidebarView_Previews: PreviewProvider { static var previews: some View { + let userCollection1 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + let userCollection2 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + let userCollection3 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + userCollection1.title = "Collection 1" + userCollection2.title = "Collection 2" + userCollection3.title = "Collection 3" + let model = WriteFreelyModel() - model.collections = CollectionListModel(with: [userCollection1, userCollection2, userCollection3]) + model.collections = CollectionListModel() + return SidebarView() .environmentObject(model) + .environment(\.managedObjectContext, PersistenceManager.persistentContainer.viewContext) } } diff --git a/Shared/PersistenceManager.swift b/Shared/PersistenceManager.swift index c5ea6ea..e7d22f4 100644 --- a/Shared/PersistenceManager.swift +++ b/Shared/PersistenceManager.swift @@ -1,49 +1,50 @@ import CoreData #if os(iOS) import UIKit #elseif os(macOS) import AppKit #endif class PersistenceManager { - let persistentContainer: NSPersistentContainer = { + static let persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "LocalStorageModel") - container.loadPersistentStores(completionHandler: { (_, error) in + container.loadPersistentStores { _, error in + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy if let error = error { fatalError("Unresolved error loading persistent store: \(error)") } - }) + } return container }() init() { let center = NotificationCenter.default #if os(iOS) let notification = UIApplication.willResignActiveNotification #elseif os(macOS) let notification = NSApplication.willResignActiveNotification #endif // We don't need to worry about removing this observer because we're targeting iOS 9+ / macOS 10.11+; the // system will clean this up the next time it would be posted to. // See: https://developer.apple.com/documentation/foundation/notificationcenter/1413994-removeobserver // And: https://developer.apple.com/documentation/foundation/notificationcenter/1407263-removeobserver // swiftlint:disable:next discarded_notification_center_observer center.addObserver(forName: notification, object: nil, queue: nil) { [weak self] _ in guard let self = self else { return } self.saveContext() } } func saveContext() { - if persistentContainer.viewContext.hasChanges { + if PersistenceManager.persistentContainer.viewContext.hasChanges { do { - try persistentContainer.viewContext.save() + try PersistenceManager.persistentContainer.viewContext.save() } catch { print("Error saving context: \(error)") } } } } diff --git a/Shared/PostCollection/CollectionListModel.swift b/Shared/PostCollection/CollectionListModel.swift index 64c7c54..4cd21cc 100644 --- a/Shared/PostCollection/CollectionListModel.swift +++ b/Shared/PostCollection/CollectionListModel.swift @@ -1,18 +1,25 @@ import SwiftUI +import CoreData class CollectionListModel: ObservableObject { - private(set) var userCollections: [PostCollection] = [] - @Published private(set) var collectionsList: [PostCollection] = [ allPostsCollection, draftsCollection ] + @Published var userCollections = [WFACollection]() - init(with userCollections: [PostCollection]) { - for userCollection in userCollections { - self.userCollections.append(userCollection) - } - collectionsList.append(contentsOf: self.userCollections) + static let allPostsCollection = PostCollection(title: "All Posts") + static let draftsCollection = PostCollection(title: "Drafts") + + init() { +// let request = WFACollection.createFetchRequest() +// request.sortDescriptors = [] +// do { +// userCollections = try PersistenceManager.persistentContainer.viewContext.fetch(request) +// } catch { +// print("Error: Failed to fetch user collections from local store") +// userCollections = [] +// } } func clearUserCollection() { userCollections = [] - collectionsList = [ allPostsCollection, draftsCollection ] + // Clear collections from CoreData store. } } diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index 1e06865..6c71177 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -1,28 +1,50 @@ import SwiftUI struct CollectionListView: View { @EnvironmentObject var model: WriteFreelyModel + @Environment(\.managedObjectContext) var moc + + @FetchRequest(entity: WFACollection.entity(), sortDescriptors: []) var collections: FetchedResults var body: some View { List { - ForEach(model.collections.collectionsList) { collection in - NavigationLink( - destination: PostListView(selectedCollection: collection) - ) { - Text(collection.title) + NavigationLink(destination: PostListView(selectedCollection: CollectionListModel.allPostsCollection)) { + Text(CollectionListModel.allPostsCollection.title) + } + NavigationLink(destination: PostListView(selectedCollection: CollectionListModel.draftsCollection)) { + Text(CollectionListModel.draftsCollection.title) + } + Section(header: Text("Your Blogs")) { + ForEach(collections, id: \.alias) { collection in + NavigationLink( + destination: PostListView(selectedCollection: PostCollection(title: collection.title)) + ) { + Text(collection.title) + } } } } .navigationTitle("Collections") .listStyle(SidebarListStyle()) } } struct CollectionSidebar_Previews: PreviewProvider { + @Environment(\.managedObjectContext) var moc + static var previews: some View { + let userCollection1 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + let userCollection2 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + let userCollection3 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + userCollection1.title = "Collection 1" + userCollection2.title = "Collection 2" + userCollection3.title = "Collection 3" + let model = WriteFreelyModel() - model.collections = CollectionListModel(with: [userCollection1, userCollection2, userCollection3]) + model.collections = CollectionListModel() + return CollectionListView() .environmentObject(model) + .environment(\.managedObjectContext, PersistenceManager.persistentContainer.viewContext) } } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index 4d79bc7..bffd1f8 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -1,137 +1,139 @@ import SwiftUI struct PostListView: View { @EnvironmentObject var model: WriteFreelyModel @State var selectedCollection: PostCollection #if os(iOS) @State private var isPresentingSettings = false #endif var body: some View { #if os(iOS) GeometryReader { geometry in List { ForEach(showPosts(for: selectedCollection)) { post in NavigationLink( destination: PostEditorView(post: post) ) { PostCellView( post: post ) } } } .environmentObject(model) .navigationTitle(selectedCollection.title) .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: { let post = Post() model.store.add(post) }, label: { Image(systemName: "square.and.pencil") }) } ToolbarItem(placement: .bottomBar) { HStack { Button(action: { isPresentingSettings = true }, label: { Image(systemName: "gear") }).sheet( isPresented: $isPresentingSettings, onDismiss: { isPresentingSettings = false }, content: { SettingsView(isPresented: $isPresentingSettings) } ) .padding(.leading) Spacer() Text(pluralizedPostCount(for: showPosts(for: selectedCollection))) .foregroundColor(.secondary) Spacer() Button(action: { reloadFromServer() }, label: { Image(systemName: "arrow.clockwise") }) .disabled(!model.account.isLoggedIn) } .padding() .frame(width: geometry.size.width) } } } #else //if os(macOS) List { ForEach(showPosts(for: selectedCollection)) { post in NavigationLink( destination: PostEditorView(post: post) ) { PostCellView( post: post ) } } } .navigationTitle(selectedCollection.title) .navigationSubtitle(pluralizedPostCount(for: showPosts(for: selectedCollection))) .toolbar { Button(action: { let post = Post() model.store.add(post) }, label: { Image(systemName: "square.and.pencil") }) Button(action: { reloadFromServer() }, label: { Image(systemName: "arrow.clockwise") }) .disabled(!model.account.isLoggedIn) } #endif } private func pluralizedPostCount(for posts: [Post]) -> String { if posts.count == 1 { return "1 post" } else { return "\(posts.count) posts" } } private func showPosts(for collection: PostCollection) -> [Post] { - if collection == allPostsCollection { - return model.store.posts + var posts: [Post] + if collection == CollectionListModel.allPostsCollection { + posts = model.store.posts + } else if collection == CollectionListModel.draftsCollection { + posts = model.store.posts.filter { $0.collection == nil } } else { - return model.store.posts.filter { - $0.collection.title == collection.title - } + posts = model.store.posts.filter { $0.collection?.title == collection.title } } + return posts } private func reloadFromServer() { DispatchQueue.main.async { model.collections.clearUserCollection() model.fetchUserCollections() model.fetchUserPosts() } } } struct PostList_Previews: PreviewProvider { static var previews: some View { let model = WriteFreelyModel() for post in testPostData { model.store.add(post) } return Group { - PostListView(selectedCollection: allPostsCollection) + PostListView(selectedCollection: CollectionListModel.allPostsCollection) .environmentObject(model) } } } diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift index 63c1182..3865a58 100644 --- a/Shared/WriteFreely_MultiPlatformApp.swift +++ b/Shared/WriteFreely_MultiPlatformApp.swift @@ -1,41 +1,42 @@ import SwiftUI @main struct WriteFreely_MultiPlatformApp: App { @StateObject private var model = WriteFreelyModel() #if os(macOS) @State private var selectedTab = 0 #endif var body: some Scene { WindowGroup { ContentView() .environmentObject(model) + .environment(\.managedObjectContext, PersistenceManager.persistentContainer.viewContext) // .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info. } #if os(macOS) Settings { TabView(selection: $selectedTab) { MacAccountView() .environmentObject(model) .tabItem { Image(systemName: "person.crop.circle") Text("Account") } .tag(0) MacPreferencesView(preferences: model.preferences) .tabItem { Image(systemName: "gear") Text("Preferences") } .tag(1) } .frame(minWidth: 300, maxWidth: 300, minHeight: 200, maxHeight: 200) .padding() // .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info. } #endif } } diff --git a/WFACollection+CoreDataProperties.swift b/WFACollection+CoreDataProperties.swift index 7aceb40..8c8ca02 100644 --- a/WFACollection+CoreDataProperties.swift +++ b/WFACollection+CoreDataProperties.swift @@ -1,22 +1,22 @@ import Foundation import CoreData extension WFACollection { - @nonobjc public class func fetchRequest() -> NSFetchRequest { + @nonobjc public class func createFetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "WFACollection") } @NSManaged public var alias: String? @NSManaged public var blogDescription: String? @NSManaged public var email: String? @NSManaged public var isPublic: Bool @NSManaged public var styleSheet: String? - @NSManaged public var title: String? + @NSManaged public var title: String @NSManaged public var url: String? } extension WFACollection: Identifiable { }