Skip to content

Commit 3751118

Browse files
Merge pull request #32 from writeas/reload-from-server
Implement reload-from-server
2 parents bc8ae19 + 8205673 commit 3751118

File tree

8 files changed

+282
-11
lines changed

8 files changed

+282
-11
lines changed

Shared/Account/AccountLogoutView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ struct AccountLogoutView: View {
77
VStack {
88
Spacer()
99
VStack {
10-
Text("Logged in as \(model.account.username ?? "Anonymous")")
10+
Text("Logged in as \(model.account.username)")
1111
Text("on \(model.account.server)")
1212
}
1313
Spacer()

Shared/Models/Post.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ enum PostStatus {
77
case published
88
}
99

10-
class Post: Identifiable, ObservableObject {
10+
class Post: Identifiable, ObservableObject, Hashable {
1111
@Published var wfPost: WFPost
1212
@Published var status: PostStatus
1313
@Published var collection: PostCollection
14+
@Published var hasNewerRemoteCopy: Bool = false
1415

1516
let id = UUID()
1617

@@ -38,6 +39,16 @@ class Post: Identifiable, ObservableObject {
3839
}
3940
}
4041

42+
extension Post {
43+
static func == (lhs: Post, rhs: Post) -> Bool {
44+
return lhs.id == rhs.id
45+
}
46+
47+
func hash(into hasher: inout Hasher) {
48+
hasher.combine(id)
49+
}
50+
}
51+
4152
#if DEBUG
4253
let testPost = Post(
4354
title: "Test Post Title",

Shared/Models/PostStore.swift

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import WriteFreely
23

34
struct PostStore {
45
var posts: [Post]
@@ -11,7 +12,55 @@ struct PostStore {
1112
posts.append(post)
1213
}
1314

14-
mutating func purge() {
15+
mutating func purgeAllPosts() {
1516
posts = []
1617
}
18+
19+
mutating func update(_ post: Post) {
20+
// Find the local copy in the store
21+
let localCopy = posts.first(where: { $0.id == post.id })
22+
23+
// If there's a local copy, update the updatedDate property of its WFPost
24+
if let localCopy = localCopy {
25+
localCopy.wfPost.updatedDate = Date()
26+
} else {
27+
print("Error: Local copy not found")
28+
}
29+
}
30+
31+
mutating func replace(post: Post, with fetchedPost: WFPost) {
32+
// Find the local copy in the store.
33+
let localCopy = posts.first(where: { $0.id == post.id })
34+
35+
// Replace the local copy's wfPost property with the fetched copy.
36+
if let localCopy = localCopy {
37+
localCopy.wfPost = fetchedPost
38+
DispatchQueue.main.async {
39+
localCopy.hasNewerRemoteCopy = false
40+
localCopy.status = .published
41+
}
42+
} else {
43+
print("Error: Local copy not found")
44+
}
45+
}
46+
47+
mutating func updateStore(with fetchedPosts: [Post]) {
48+
for fetchedPost in fetchedPosts {
49+
// Find the local copy in the store.
50+
let localCopy = posts.first(where: { $0.wfPost.postId == fetchedPost.wfPost.postId })
51+
52+
// If there's a local copy, check which is newer; if not, add the fetched post to the store.
53+
if let localCopy = localCopy {
54+
// We do not discard the local copy; we simply set the hasNewerRemoteCopy flag accordingly.
55+
if let remoteCopyUpdatedDate = fetchedPost.wfPost.updatedDate,
56+
let localCopyUpdatedDate = localCopy.wfPost.updatedDate {
57+
localCopy.hasNewerRemoteCopy = remoteCopyUpdatedDate > localCopyUpdatedDate
58+
} else {
59+
print("Error: could not determine which copy of post is newer")
60+
}
61+
} else {
62+
add(fetchedPost)
63+
}
64+
}
65+
}
1766
}

Shared/Models/WriteFreelyModel.swift

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class WriteFreelyModel: ObservableObject {
1010
@Published var store = PostStore()
1111
@Published var collections = CollectionListModel(with: [])
1212
@Published var isLoggingIn: Bool = false
13+
@Published var selectedPost: Post?
1314

1415
private var client: WFClient?
1516
private let defaults = UserDefaults.standard
@@ -26,6 +27,25 @@ class WriteFreelyModel: ObservableObject {
2627

2728
DispatchQueue.main.async {
2829
self.account.restoreState()
30+
if self.account.isLoggedIn {
31+
guard let serverURL = URL(string: self.account.server) else {
32+
print("Server URL not found")
33+
return
34+
}
35+
guard let token = self.fetchTokenFromKeychain(
36+
username: self.account.username,
37+
server: self.account.server
38+
) else {
39+
print("Could not fetch token from Keychain")
40+
return
41+
}
42+
self.account.login(WFUser(token: token, username: self.account.username))
43+
self.client = WFClient(for: serverURL)
44+
self.client?.user = self.account.user
45+
self.collections.clearUserCollection()
46+
self.fetchUserCollections()
47+
self.fetchUserPosts()
48+
}
2949
}
3050
}
3151
}
@@ -80,6 +100,15 @@ extension WriteFreelyModel {
80100
)
81101
}
82102
}
103+
104+
func updateFromServer(post: Post) {
105+
guard let loggedInClient = client else { return }
106+
guard let postId = post.wfPost.postId else { return }
107+
DispatchQueue.main.async {
108+
self.selectedPost = post
109+
}
110+
loggedInClient.getPost(byId: postId, completion: updateFromServerHandler)
111+
}
83112
}
84113

85114
private extension WriteFreelyModel {
@@ -121,7 +150,7 @@ private extension WriteFreelyModel {
121150
DispatchQueue.main.async {
122151
self.account.logout()
123152
self.collections.clearUserCollection()
124-
self.store.purge()
153+
self.store.purgeAllPosts()
125154
}
126155
} catch {
127156
print("Something went wrong purging the token from the Keychain.")
@@ -136,7 +165,7 @@ private extension WriteFreelyModel {
136165
DispatchQueue.main.async {
137166
self.account.logout()
138167
self.collections.clearUserCollection()
139-
self.store.purge()
168+
self.store.purgeAllPosts()
140169
}
141170
} catch {
142171
print("Something went wrong purging the token from the Keychain.")
@@ -176,6 +205,7 @@ private extension WriteFreelyModel {
176205
func fetchUserPostsHandler(result: Result<[WFPost], Error>) {
177206
do {
178207
let fetchedPosts = try result.get()
208+
var fetchedPostsArray: [Post] = []
179209
for fetchedPost in fetchedPosts {
180210
var post: Post
181211
if let matchingAlias = fetchedPost.collectionAlias {
@@ -186,9 +216,10 @@ private extension WriteFreelyModel {
186216
} else {
187217
post = Post(wfPost: fetchedPost)
188218
}
189-
DispatchQueue.main.async {
190-
self.store.add(post)
191-
}
219+
fetchedPostsArray.append(post)
220+
}
221+
DispatchQueue.main.async {
222+
self.store.updateStore(with: fetchedPostsArray)
192223
}
193224
} catch {
194225
print(error)
@@ -209,6 +240,18 @@ private extension WriteFreelyModel {
209240
print(error)
210241
}
211242
}
243+
244+
func updateFromServerHandler(result: Result<WFPost, Error>) {
245+
do {
246+
let fetchedPost = try result.get()
247+
DispatchQueue.main.async {
248+
guard let selectedPost = self.selectedPost else { return }
249+
self.store.replace(post: selectedPost, with: fetchedPost)
250+
}
251+
} catch {
252+
print(error)
253+
}
254+
}
212255
}
213256

214257
private extension WriteFreelyModel {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import SwiftUI
2+
3+
struct PostEditorStatusToolbarView: View {
4+
#if os(iOS)
5+
@Environment(\.horizontalSizeClass) var horizontalSizeClass
6+
#endif
7+
@EnvironmentObject var model: WriteFreelyModel
8+
9+
@ObservedObject var post: Post
10+
11+
var body: some View {
12+
if post.hasNewerRemoteCopy {
13+
#if os(iOS)
14+
if horizontalSizeClass == .compact {
15+
VStack {
16+
PostStatusBadgeView(post: post)
17+
HStack {
18+
Text("⚠️ Newer copy on server. Replace local copy?")
19+
.font(.caption)
20+
.foregroundColor(.secondary)
21+
Button(action: {
22+
model.updateFromServer(post: post)
23+
}, label: {
24+
Image(systemName: "square.and.arrow.down")
25+
})
26+
}
27+
.padding(.bottom)
28+
}
29+
.padding(.top)
30+
} else {
31+
HStack {
32+
PostStatusBadgeView(post: post)
33+
.padding(.trailing)
34+
Text("⚠️ Newer copy on server. Replace local copy?")
35+
.font(.callout)
36+
.foregroundColor(.secondary)
37+
Button(action: {
38+
model.updateFromServer(post: post)
39+
}, label: {
40+
Image(systemName: "square.and.arrow.down")
41+
})
42+
}
43+
}
44+
#else
45+
HStack {
46+
PostStatusBadgeView(post: post)
47+
.padding(.trailing)
48+
Text("⚠️ Newer copy on server. Replace local copy?")
49+
.font(.callout)
50+
.foregroundColor(.secondary)
51+
Button(action: {
52+
model.updateFromServer(post: post)
53+
}, label: {
54+
Image(systemName: "square.and.arrow.down")
55+
})
56+
}
57+
#endif
58+
} else {
59+
PostStatusBadgeView(post: post)
60+
}
61+
}
62+
}
63+
64+
struct ToolbarView_LocalPreviews: PreviewProvider {
65+
static var previews: some View {
66+
let model = WriteFreelyModel()
67+
let post = testPost
68+
return PostEditorStatusToolbarView(post: post)
69+
.environmentObject(model)
70+
}
71+
}
72+
73+
struct ToolbarView_RemotePreviews: PreviewProvider {
74+
static var previews: some View {
75+
let model = WriteFreelyModel()
76+
let newerRemotePost = Post(
77+
title: testPost.wfPost.title ?? "",
78+
body: testPost.wfPost.body,
79+
createdDate: testPost.wfPost.createdDate ?? Date(),
80+
status: testPost.status,
81+
collection: testPost.collection
82+
)
83+
newerRemotePost.hasNewerRemoteCopy = true
84+
return PostEditorStatusToolbarView(post: newerRemotePost)
85+
.environmentObject(model)
86+
}
87+
}
88+
89+
#if os(iOS)
90+
struct ToolbarView_CompactLocalPreviews: PreviewProvider {
91+
static var previews: some View {
92+
let model = WriteFreelyModel()
93+
let post = testPost
94+
return PostEditorStatusToolbarView(post: post)
95+
.environmentObject(model)
96+
.environment(\.horizontalSizeClass, .compact)
97+
}
98+
}
99+
#endif
100+
101+
#if os(iOS)
102+
struct ToolbarView_CompactRemotePreviews: PreviewProvider {
103+
static var previews: some View {
104+
let model = WriteFreelyModel()
105+
let newerRemotePost = Post(
106+
title: testPost.wfPost.title ?? "",
107+
body: testPost.wfPost.body,
108+
createdDate: testPost.wfPost.createdDate ?? Date(),
109+
status: testPost.status,
110+
collection: testPost.collection
111+
)
112+
newerRemotePost.hasNewerRemoteCopy = true
113+
return PostEditorStatusToolbarView(post: newerRemotePost)
114+
.environmentObject(model)
115+
.environment(\.horizontalSizeClass, .compact)
116+
}
117+
}
118+
#endif

Shared/PostEditor/PostEditorView.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ struct PostEditorView: View {
2929
.padding()
3030
.toolbar {
3131
ToolbarItem(placement: .status) {
32-
PostStatusBadgeView(post: post)
32+
PostEditorStatusToolbarView(post: post)
3333
}
3434
ToolbarItem(placement: .primaryAction) {
3535
Button(action: {
@@ -47,6 +47,13 @@ struct PostEditorView: View {
4747
addNewPostToStore()
4848
}
4949
})
50+
.onDisappear(perform: {
51+
if post.status == .edited {
52+
DispatchQueue.main.async {
53+
model.store.update(post)
54+
}
55+
}
56+
})
5057
}
5158

5259
private func checkIfNewPost() {
@@ -68,9 +75,24 @@ struct PostEditorView_NewLocalDraftPreviews: PreviewProvider {
6875
}
6976
}
7077

71-
struct PostEditorView_ExistingPostPreviews: PreviewProvider {
78+
struct PostEditorView_NewerLocalPostPreviews: PreviewProvider {
79+
static var previews: some View {
80+
return PostEditorView(post: testPost)
81+
.environmentObject(WriteFreelyModel())
82+
}
83+
}
84+
85+
struct PostEditorView_NewerRemotePostPreviews: PreviewProvider {
7286
static var previews: some View {
73-
PostEditorView(post: testPostData[0])
87+
let newerRemotePost = Post(
88+
title: testPost.wfPost.title ?? "",
89+
body: testPost.wfPost.body,
90+
createdDate: testPost.wfPost.createdDate ?? Date(),
91+
status: testPost.status,
92+
collection: testPost.collection
93+
)
94+
newerRemotePost.hasNewerRemoteCopy = true
95+
return PostEditorView(post: newerRemotePost)
7496
.environmentObject(WriteFreelyModel())
7597
}
7698
}

0 commit comments

Comments
 (0)