diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index baf1dd3..b6a17b9 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -1,360 +1,367 @@ import Foundation import WriteFreely import Security // MARK: - WriteFreelyModel class WriteFreelyModel: ObservableObject { @Published var account = AccountModel() @Published var preferences = PreferencesModel() @Published var store = PostListModel() @Published var collections = CollectionListModel() @Published var isLoggingIn: Bool = false @Published var selectedPost: WFAPost? 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 if self.collections.userCollections.count == 0 { 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: WFAPost) { guard let loggedInClient = client else { return } var wfPost = WFPost( - body: post.body ?? "", - title: post.title, + body: post.body, + title: post.title.isEmpty ? "" : post.title, appearance: post.appearance, language: post.language, rtl: post.rtl, createdDate: post.createdDate ) if let existingPostId = post.postId { // This is an existing post. wfPost.postId = post.postId wfPost.slug = post.slug wfPost.updatedDate = post.updatedDate wfPost.collectionAlias = post.collectionAlias loggedInClient.updatePost( postId: existingPostId, updatedPost: wfPost, completion: publishHandler ) } else { // This is a new local draft. loggedInClient.createPost( post: wfPost, in: post.collectionAlias, completion: publishHandler ) } } func updateFromServer(post: WFAPost) { guard let loggedInClient = client else { return } guard let postId = post.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.collections.clearUserCollection() self.account.logout() 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() for fetchedCollection in fetchedCollections { 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 { PersistenceManager().saveContext() } } catch { print(error) } } func fetchUserPostsHandler(result: Result<[WFPost], Error>) { do { let fetchedPosts = try result.get() for fetchedPost in fetchedPosts { - let managedPost = WFAPost(context: PersistenceManager.persistentContainer.viewContext) - managedPost.postId = fetchedPost.postId - managedPost.slug = fetchedPost.slug - managedPost.appearance = fetchedPost.appearance - managedPost.language = fetchedPost.language - managedPost.rtl = fetchedPost.rtl ?? false - managedPost.createdDate = fetchedPost.createdDate - managedPost.updatedDate = fetchedPost.updatedDate - managedPost.title = fetchedPost.title - managedPost.body = fetchedPost.body - managedPost.collectionAlias = fetchedPost.collectionAlias - managedPost.status = PostStatus.published.rawValue + // For each fetched post, we + // 1. check to see if a matching post exists + if let managedPost = store.posts.first(where: { $0.postId == fetchedPost.postId }) { + // If it exists, we set the hasNewerRemoteCopy flag as appropriate. + if let fetchedPostUpdatedDate = fetchedPost.updatedDate, + let localPostUpdatedDate = managedPost.updatedDate { + managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate + } else { + print("Error: could not determine which copy of post is newer") + } + } else { + // If it doesn't exist, we create the managed object. + let managedPost = WFAPost(context: PersistenceManager.persistentContainer.viewContext) + managedPost.postId = fetchedPost.postId + managedPost.slug = fetchedPost.slug + managedPost.appearance = fetchedPost.appearance + managedPost.language = fetchedPost.language + managedPost.rtl = fetchedPost.rtl ?? false + managedPost.createdDate = fetchedPost.createdDate + managedPost.updatedDate = fetchedPost.updatedDate + managedPost.title = fetchedPost.title ?? "" + managedPost.body = fetchedPost.body + managedPost.collectionAlias = fetchedPost.collectionAlias + managedPost.status = PostStatus.published.rawValue + } } DispatchQueue.main.async { PersistenceManager().saveContext() } } catch { print(error) } } func publishHandler(result: Result) { do { let fetchedPost = try result.get() let foundPostIndex = store.posts.firstIndex(where: { $0.title == fetchedPost.title && $0.body == fetchedPost.body }) guard let index = foundPostIndex else { return } let cachedPost = self.store.posts[index] cachedPost.appearance = fetchedPost.appearance cachedPost.body = fetchedPost.body cachedPost.collectionAlias = fetchedPost.collectionAlias cachedPost.createdDate = fetchedPost.createdDate cachedPost.language = fetchedPost.language cachedPost.postId = fetchedPost.postId cachedPost.rtl = fetchedPost.rtl ?? false cachedPost.slug = fetchedPost.slug cachedPost.status = PostStatus.published.rawValue - cachedPost.title = fetchedPost.title + cachedPost.title = fetchedPost.title ?? "" cachedPost.updatedDate = fetchedPost.updatedDate DispatchQueue.main.async { PersistenceManager().saveContext() } } catch { print(error) } } func updateFromServerHandler(result: Result) { do { let fetchedPost = try result.get() guard let cachedPost = self.selectedPost else { return } cachedPost.appearance = fetchedPost.appearance cachedPost.body = fetchedPost.body cachedPost.collectionAlias = fetchedPost.collectionAlias cachedPost.createdDate = fetchedPost.createdDate cachedPost.language = fetchedPost.language cachedPost.postId = fetchedPost.postId cachedPost.rtl = fetchedPost.rtl ?? false cachedPost.slug = fetchedPost.slug cachedPost.status = PostStatus.published.rawValue - cachedPost.title = fetchedPost.title + cachedPost.title = fetchedPost.title ?? "" cachedPost.updatedDate = fetchedPost.updatedDate + cachedPost.hasNewerRemoteCopy = false DispatchQueue.main.async { PersistenceManager().saveContext() } } 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/PostEditor/PostEditorStatusToolbarView.swift b/Shared/PostEditor/PostEditorStatusToolbarView.swift index 85c5e65..7f19f7a 100644 --- a/Shared/PostEditor/PostEditorStatusToolbarView.swift +++ b/Shared/PostEditor/PostEditorStatusToolbarView.swift @@ -1,136 +1,136 @@ import SwiftUI struct PostEditorStatusToolbarView: View { #if os(iOS) @Environment(\.horizontalSizeClass) var horizontalSizeClass #endif @EnvironmentObject var model: WriteFreelyModel @ObservedObject var post: WFAPost var body: some View { if post.hasNewerRemoteCopy { #if os(iOS) if horizontalSizeClass == .compact { VStack { PostStatusBadgeView(post: post) HStack { Text("⚠️ Newer copy on server. Replace local copy?") .font(.caption) .foregroundColor(.secondary) Button(action: { - model.updateFromServer(post: post) + model.updateFromServer(post: post) // FIXME: This shouldn't change post status after update }, label: { Image(systemName: "square.and.arrow.down") }) } .padding(.bottom) } .padding(.top) } else { HStack { PostStatusBadgeView(post: post) .padding(.trailing) Text("⚠️ Newer copy on server. Replace local copy?") .font(.callout) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } } #else HStack { PostStatusBadgeView(post: post) .padding(.trailing) Text("⚠️ Newer copy on server. Replace local copy?") .font(.callout) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } #endif } else { PostStatusBadgeView(post: post) } } } //#if DEBUG //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() //) //#endif // //struct ToolbarView_LocalPreviews: PreviewProvider { // static var previews: some View { // let model = WriteFreelyModel() // let post = testPost // return PostEditorStatusToolbarView(post: post) // .environmentObject(model) // } //} // //struct ToolbarView_RemotePreviews: PreviewProvider { // static var previews: some View { // let model = WriteFreelyModel() // let newerRemotePost = Post( // title: testPost.wfPost.title ?? "", // body: testPost.wfPost.body, // createdDate: testPost.wfPost.createdDate ?? Date(), // status: testPost.status, // collection: testPost.collection // ) // newerRemotePost.hasNewerRemoteCopy = true // return PostEditorStatusToolbarView(post: newerRemotePost) // .environmentObject(model) // } //} // //#if os(iOS) //struct ToolbarView_CompactLocalPreviews: PreviewProvider { // static var previews: some View { // let model = WriteFreelyModel() // let post = testPost // return PostEditorStatusToolbarView(post: post) // .environmentObject(model) // .environment(\.horizontalSizeClass, .compact) // } //} //#endif // //#if os(iOS) //struct ToolbarView_CompactRemotePreviews: PreviewProvider { // static var previews: some View { // let model = WriteFreelyModel() // let newerRemotePost = Post( // title: testPost.wfPost.title ?? "", // body: testPost.wfPost.body, // createdDate: testPost.wfPost.createdDate ?? Date(), // status: testPost.status, // collection: testPost.collection // ) // newerRemotePost.hasNewerRemoteCopy = true // return PostEditorStatusToolbarView(post: newerRemotePost) // .environmentObject(model) // .environment(\.horizontalSizeClass, .compact) // } //} //#endif diff --git a/Shared/PostEditor/PostEditorView.swift b/Shared/PostEditor/PostEditorView.swift index 9fcd55d..b638e01 100644 --- a/Shared/PostEditor/PostEditorView.swift +++ b/Shared/PostEditor/PostEditorView.swift @@ -1,87 +1,77 @@ import SwiftUI struct PostEditorView: View { @EnvironmentObject var model: WriteFreelyModel - @Environment(\.managedObjectContext) var moc @ObservedObject var post: WFAPost - @State private var postTitle = "" - @State private var postBody = "" - var body: some View { VStack { - TextEditor(text: $postTitle) + TextEditor(text: $post.title) .font(.title) .frame(height: 100) - .onChange(of: postTitle) { _ in - if post.status == PostStatus.published.rawValue && post.title != postTitle { + .onChange(of: post.title) { _ in + if post.status == PostStatus.published.rawValue { post.status = PostStatus.edited.rawValue } - post.title = postTitle } - TextEditor(text: $postBody) + TextEditor(text: $post.body) .font(.body) - .onChange(of: postBody) { _ in + .onChange(of: post.body) { _ in if post.status == PostStatus.published.rawValue { post.status = PostStatus.edited.rawValue } - post.body = postBody } } .padding() .toolbar { ToolbarItem(placement: .status) { PostEditorStatusToolbarView(post: post) } ToolbarItem(placement: .primaryAction) { Button(action: { model.publish(post: post) post.status = PostStatus.published.rawValue }, label: { Image(systemName: "paperplane") }) } } - .onAppear(perform: { - postTitle = post.title ?? "" - postBody = post.body ?? "" - }) .onDisappear(perform: { if post.status == PostStatus.edited.rawValue { DispatchQueue.main.async { PersistenceManager().saveContext() } } }) } } //struct PostEditorView_NewLocalDraftPreviews: PreviewProvider { // static var previews: some View { // PostEditorView(post: Post()) // .environmentObject(WriteFreelyModel()) // } //} // //struct PostEditorView_NewerLocalPostPreviews: PreviewProvider { // static var previews: some View { // return PostEditorView(post: testPost) // .environmentObject(WriteFreelyModel()) // } //} // //struct PostEditorView_NewerRemotePostPreviews: PreviewProvider { // static var previews: some View { // let newerRemotePost = Post( // title: testPost.wfPost.title ?? "", // body: testPost.wfPost.body, // createdDate: testPost.wfPost.createdDate ?? Date(), // status: testPost.status, // collection: testPost.collection // ) // newerRemotePost.hasNewerRemoteCopy = true // return PostEditorView(post: newerRemotePost) // .environmentObject(WriteFreelyModel()) // } //} diff --git a/Shared/PostList/PostCellView.swift b/Shared/PostList/PostCellView.swift index 535260e..3f65070 100644 --- a/Shared/PostList/PostCellView.swift +++ b/Shared/PostList/PostCellView.swift @@ -1,40 +1,40 @@ import SwiftUI struct PostCellView: View { @ObservedObject var post: WFAPost var body: some View { HStack { VStack(alignment: .leading) { - Text(post.title ?? "") + Text(post.title) .font(.headline) .lineLimit(1) Text(buildDateString(from: post.createdDate ?? Date())) .font(.caption) .lineLimit(1) } Spacer() PostStatusBadgeView(post: post) } .padding(5) } func buildDateString(from date: Date) -> String { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .long dateFormatter.timeStyle = .short return dateFormatter.string(from: date) } } //struct PostCell_Previews: PreviewProvider { // static var previews: some View { // let testPost = Post( // title: "Test Post Title", // body: "Here's some cool sample body text.", // createdDate: Date() // ) // return PostCellView(post: testPost) // } //} diff --git a/Shared/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift index 3f2d9c9..29ad70c 100644 --- a/Shared/PostList/PostListModel.swift +++ b/Shared/PostList/PostListModel.swift @@ -1,91 +1,39 @@ import Foundation import WriteFreely import CoreData class PostListModel: ObservableObject { @Published var posts = [WFAPost]() init() { loadCachedPosts() } func loadCachedPosts() { let request = WFAPost.createFetchRequest() let sort = NSSortDescriptor(key: "createdDate", ascending: false) request.sortDescriptors = [sort] posts = [] do { let cachedPosts = try PersistenceManager.persistentContainer.viewContext.fetch(request) posts.append(contentsOf: cachedPosts) } catch { print("Error: Failed to fetch cached posts.") } } func purgeAllPosts() { posts = [] let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "WFAPost") let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { try PersistenceManager.persistentContainer.persistentStoreCoordinator.execute( deleteRequest, with: PersistenceManager.persistentContainer.viewContext ) } catch { print("Error: Failed to purge cached posts.") } } - -// func add(_ post: WFAPost) { -// posts.append(post) -// } - -// func update(_ post: WFAPost) { -// // Find the local copy in the store -// let localCopy = posts.first(where: { $0.id == post.id }) -// -// // If there's a local copy, update the updatedDate property of its WFPost -// if let localCopy = localCopy { -// localCopy.wfPost.updatedDate = Date() -// } else { -// print("Error: Local copy not found") -// } -// } - -// func replace(post: Post, with fetchedPost: WFPost) { -// // Find the local copy in the store. -// let localCopy = posts.first(where: { $0.id == post.id }) -// -// // Replace the local copy's wfPost property with the fetched copy. -// if let localCopy = localCopy { -// localCopy.wfPost = fetchedPost -// DispatchQueue.main.async { -// localCopy.hasNewerRemoteCopy = false -// localCopy.status = .published -// } -// } else { -// print("Error: Local copy not found") -// } -// } - -// func updateStore(with fetchedPosts: [Post]) { -// for fetchedPost in fetchedPosts { -// // Find the local copy in the store. -// let localCopy = posts.first(where: { $0.wfPost.postId == fetchedPost.wfPost.postId }) -// -// // If there's a local copy, check which is newer; if not, add the fetched post to the store. -// if let localCopy = localCopy { -// // We do not discard the local copy; we simply set the hasNewerRemoteCopy flag accordingly. -// if let remoteCopyUpdatedDate = fetchedPost.wfPost.updatedDate, -// let localCopyUpdatedDate = localCopy.wfPost.updatedDate { -// localCopy.hasNewerRemoteCopy = remoteCopyUpdatedDate > localCopyUpdatedDate -// } else { -// print("Error: could not determine which copy of post is newer") -// } -// } else { -// add(fetchedPost) -// } -// } -// } } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index 81d23b6..c3571b0 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -1,206 +1,204 @@ import SwiftUI struct PostListView: View { @EnvironmentObject var model: WriteFreelyModel @Environment(\.managedObjectContext) var moc @FetchRequest( entity: WFAPost.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \WFAPost.createdDate, ascending: true)] ) var posts: FetchedResults @State var selectedCollection: WFACollection? @State var showAllPosts: Bool = false #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( showAllPosts ? "All Posts" : selectedCollection?.title ?? ( model.account.server == "https://write.as" ? "Anonymous" : "Drafts" ) ) .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: { createNewLocalDraft() }, 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( showAllPosts ? "All Posts" : selectedCollection?.title ?? ( model.account.server == "https://write.as" ? "Anonymous" : "Drafts" ) ) .navigationSubtitle(pluralizedPostCount(for: showPosts(for: selectedCollection))) .toolbar { Button(action: { createNewLocalDraft() }, label: { Image(systemName: "square.and.pencil") }) Button(action: { reloadFromServer() }, label: { Image(systemName: "arrow.clockwise") }) .disabled(!model.account.isLoggedIn) } #endif } private func pluralizedPostCount(for posts: [WFAPost]) -> String { if posts.count == 1 { return "1 post" } else { return "\(posts.count) posts" } } private func showPosts(for collection: WFACollection?) -> [WFAPost] { if showAllPosts { return model.store.posts } else { if let selectedCollection = collection { return model.store.posts.filter { $0.collectionAlias == selectedCollection.alias } } else { return model.store.posts.filter { $0.collectionAlias == nil } } } } private func reloadFromServer() { DispatchQueue.main.async { model.collections.clearUserCollection() model.fetchUserCollections() model.fetchUserPosts() } } private func createNewLocalDraft() { - let post = Post() let managedPost = WFAPost(context: PersistenceManager.persistentContainer.viewContext) - managedPost.createdDate = post.wfPost.createdDate - managedPost.title = post.wfPost.title - managedPost.body = post.wfPost.body + managedPost.createdDate = Date() + managedPost.title = "" + managedPost.body = "" managedPost.status = PostStatus.local.rawValue DispatchQueue.main.async { -// model.store.add(post) PersistenceManager().saveContext() } } } //struct PostList_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 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 // ) // ] // // let model = WriteFreelyModel() // for post in testPostData { // model.store.add(post) // } // return Group { // PostListView(selectedCollection: userCollection1) // .environmentObject(model) // } // } //} diff --git a/WFAPost+CoreDataProperties.swift b/WFAPost+CoreDataProperties.swift index d7ef424..48d4d2b 100644 --- a/WFAPost+CoreDataProperties.swift +++ b/WFAPost+CoreDataProperties.swift @@ -1,35 +1,35 @@ // // WFAPost+CoreDataProperties.swift // WriteFreely-MultiPlatform // // Created by Angelo Stavrow on 2020-09-08. // // import Foundation import CoreData extension WFAPost { @nonobjc public class func createFetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "WFAPost") } @NSManaged public var appearance: String? - @NSManaged public var body: String? + @NSManaged public var body: String @NSManaged public var collectionAlias: String? @NSManaged public var createdDate: Date? @NSManaged public var language: String? @NSManaged public var postId: String? @NSManaged public var rtl: Bool @NSManaged public var slug: String? @NSManaged public var status: Int32 - @NSManaged public var title: String? + @NSManaged public var title: String @NSManaged public var updatedDate: Date? @NSManaged public var hasNewerRemoteCopy: Bool } extension WFAPost: Identifiable { }