Skip to content

Commit

Permalink
Improve updates and update frequency
Browse files Browse the repository at this point in the history
  • Loading branch information
0xLeif committed Sep 27, 2024
1 parent 2761a8a commit 44a2331
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 75 deletions.
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import PackageDescription
let package = Package(
name: "AppState",
platforms: [
.iOS(.v15),
.watchOS(.v8),
.macOS(.v11),
.tvOS(.v15),
.iOS(.v16),
.watchOS(.v9),
.macOS(.v12),
.tvOS(.v16),
.visionOS(.v1)
],
products: [
Expand Down
3 changes: 2 additions & 1 deletion Sources/AppState/Application/Application+internal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ extension Application {
"AppState/Application+FileState.swift",
"AppState/Application+CloudState.swift",
"AppState/Application+CloudStateViewModel.swift",
"AppState/CloudStateStore.swift"
"AppState/CloudStateStore.swift",
"AppState/FilePresenter.swift"
]
let isFileIDValue: Bool = excludedFileIDs.contains(fileID.description) == false

Expand Down
63 changes: 55 additions & 8 deletions Sources/AppState/Application/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,28 +84,75 @@ open class Application: NSObject {
)
}

open func cloudStoreItemDidChange(url: URL) {
open func cloudStoreItemDidChange(
scope: Scope,
url: URL,
modificationDate: Date?,
attributeModificationDate: Date?,
completion: () -> Void
) {
var externalChangesState: State<[String: ExternalChange]> = externalChanges

let lastChange = externalChangesState.value[scope.key]
guard
lastChange?.modificationDate != modificationDate,
lastChange?.attributeModificationDate != attributeModificationDate
else {
return
}

var debugMessage: String = "☁️ CloudState was changed externally \(scope.key)"

if lastChange?.modificationDate != nil || modificationDate != nil {
let lastExternalChangeMessage = lastChange?.modificationDate
.formatted(date: .long, time: .complete) ?? "nil"

let newExternalChangeMessage = modificationDate?
.formatted(date: .long, time: .complete) ?? "nil"

debugMessage += """
\n\t- Content Modification Dates:
\t\t- Last: \(lastExternalChangeMessage)
\t\t- New: \(newExternalChangeMessage)
"""
}

if lastChange?.attributeModificationDate != nil || attributeModificationDate != nil {
let lastExternalChangeMessage = lastChange?.attributeModificationDate
.formatted(date: .long, time: .complete) ?? "nil"

let newExternalChangeMessage = attributeModificationDate?
.formatted(date: .long, time: .complete) ?? "nil"

debugMessage += """
\n\t- Attribute Modification Dates:
\t\t- Last: \(lastExternalChangeMessage)
\t\t- New: \(newExternalChangeMessage)
"""
}

Application.log(
debug: """
☁️ CloudState was changed externally (\(url.absoluteString))
""",
debug: debugMessage,
fileID: #fileID,
function: #function,
line: #line,
column: #column
)

var hasExternalChangesState: State<Bool> = Application.state(\.hasExternalChanges)

DispatchQueue.main.async {
self.objectWillChange.send()
hasExternalChangesState.value = true
externalChangesState.value[scope.key] = ExternalChange(
modificationDate: modificationDate ?? Date(),
attributeModificationDate: attributeModificationDate ?? Date()
)
}

completion()
}

public static func loadCloudDependencies() {
load(dependency: \.icloudStore)
load(dependency: \.icloudDocumentStore)
load(dependency: \.fileCoordinator)
}
#endif

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,48 +9,56 @@ extension Application {
@AppDependency(\.icloudDocumentStore) private var cloudDocumentStore: CloudStateStore

@Published var value: Value?

private let scope: Scope
private var filePresenter: FilePresenter<CloudStateViewModel<Value>>?

init(scope: Scope) {
self.scope = scope
Task {
self.filePresenter = await cloudDocumentStore.startMonitoringFile(scope: scope)
self.filePresenter?.observedObject = self
}

Task {
guard
guard
let viewModel = await cloudDocumentStore.viewModels[scope.key] as? Application.CloudStateViewModel<Value>
else {
await cloudDocumentStore.update(viewModel: self, forKey: scope.key)

return
}

await MainActor.run {
value = viewModel.value
}
}
}

deinit {
guard let filePresenter else { return }

NSFileCoordinator.removeFilePresenter(filePresenter)

self.filePresenter = nil
}

func getValue(cachedValue: Value?) {
Task {
do {
await MainActor.run {
var initialFetchesState: State<Set<String>> = Application.state(\.initialFetches)
initialFetchesState.value.insert(scope.key)

var externalFetchesState: State<[String: ExternalChange]> = Application.state(\.externalChanges)
externalFetchesState.value[scope.key]?.hasCompletedChange = true
}

let cloudStoreValue: Value = try await cloudDocumentStore.get(scope)

guard cachedValue != cloudStoreValue else { return }

await MainActor.run {
objectWillChange.send()

shared.cache.set(
value: Application.State(
type: .cloud,
Expand All @@ -59,53 +67,83 @@ extension Application {
),
forKey: scope.key
)

var hasExternalChangesState: State<Bool> = Application.state(\.hasExternalChanges)
hasExternalChangesState.value = false
}
} catch {
log(
error: error,
message: "☁️ CloudState Fetching",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
switch error {
case CloudStateStore.CloudError.unitTest:
log(
debug: "⚠️ CloudState Fetching isn't allowed for unit tests.",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
default:
log(
error: error,
message: "☁️ CloudState Fetching",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
}
}
}
}

func setValue(newValue: Value) {
Task {
do {
try await cloudDocumentStore.set(scope, value: newValue)
} catch {
log(
error: error,
message: "☁️ CloudState Saving",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
switch error {
case CloudStateStore.CloudError.unitTest:
log(
debug: "⚠️ CloudState Saving isn't allowed for unit tests.",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
default:
log(
error: error,
message: "☁️ CloudState Saving",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
}
}
}
}

func removeValue() {
Task {
do {
try await cloudDocumentStore.remove(scope)
} catch {
log(
error: error,
message: "☁️ CloudState Deleting",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
switch error {
case CloudStateStore.CloudError.unitTest:
log(
debug: "⚠️ CloudState Deleting isn't allowed for unit tests.",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
default:
log(
error: error,
message: "☁️ CloudState Deleting",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,31 @@ import Foundation
import SwiftUI

extension Application {
struct ExternalChange {
var modificationDate: Date
var attributeModificationDate: Date
var hasCompletedChange: Bool

init(
modificationDate: Date,
attributeModificationDate: Date
) {
self.modificationDate = modificationDate
self.attributeModificationDate = attributeModificationDate
self.hasCompletedChange = false
}
}

var icloudDocumentStore: Dependency<CloudStateStore> {
dependency(CloudStateStore())
}

var hasExternalChanges: State<Bool> {
state(initial: true)
var initialFetches: State<Set<String>> {
state(initial: [])
}

var externalChanges: State<[String: ExternalChange]> {
state(initial: [:])
}

/// `CloudDocumentState` ...
Expand All @@ -28,7 +47,12 @@ extension Application {
as: State<Value>.self
)

if shared.value(keyPath: \.hasExternalChanges).value {
if shared.initialFetches.value.contains(scope.key) == false {
viewModel.getValue(cachedValue: cachedValue?.value)
} else if
let externalChange = shared.value(keyPath: \.externalChanges).value[scope.key],
externalChange.hasCompletedChange == false
{
viewModel.getValue(cachedValue: cachedValue?.value)
}

Expand Down
Loading

0 comments on commit 44a2331

Please sign in to comment.