Page MenuHomeMusing Studio

No OneTemporary

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<WFUser, Error>) {
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<Bool, Error>) {
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<WFPost, Error>) {
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<WFPost, Error>) {
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<NSFetchRequestResult> = 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<WFAPost>
@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<WFAPost> {
return NSFetchRequest<WFAPost>(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 {
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jan 31, 3:32 PM (17 h, 16 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3145869

Event Timeline