I'm trying to extend the Landmark SwiftUI Tutorials to persist data.
The code I added getDataFromFileURL
draw inspiration from Persisting data tutorial.
I expect for the first time, the json files won't exist in the user document directory, and the program will get data from bundle via DispatchQueue.main.async print("1. ...")
However, what actually appears to happen is that getDataFromFileURL
seems to return immediately and data is nil and the program fill data in print("5. ...")
first and print("4.a ...")
came later.
As I got this in the log output:
5. after getDataFromFileURL is still nil, loading landmarkData.json from bundle
5. after getDataFromFileURL is still nil, loading hikeData.json from bundle
1. loaded landmarkData.json from bundle
4.a success loading data from getDataFromFileURL
1. loaded hikeData.json from bundle
4.a success loading data from getDataFromFileURL
I suspect this behavior is due to DispatchQueue.global().async
is an async call and will return immediately. Since my load function is depending on the results from getDataFromFileURL, what will be the best way to handle it?
Code:
import Foundation
import Combine
final class ModelData: ObservableObject {
@Published var landmarks: [Landmark] = load("landmarkData.json")
var hikes: [Hike] = load("hikeData.json")
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarks, by: {$0.category.rawValue})
}
var features: [Landmark] {
landmarks.filter{ $0.isFeatured }
}
@Published var profile = Profile.default
}
private func fileURL(filename: String) throws -> URL {
try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appending(path: filename)
}
private func getDataFromFileURL(filename: String, completion: @escaping (Result<Data?, Error>)->Void) {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try fileURL(filename: filename)
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
DispatchQueue.main.async {
print("1. loaded \(filename) from bundle")
completion(.success(getDataFromBundle(filename)))
}
return
}
DispatchQueue.main.async {
print("2. loaded \(filename) from fileURL")
completion(.success(file.availableData))
}
} catch {
DispatchQueue.main.async {
print("3. failed to load \(filename)")
completion(.failure(error))
}
}
}
}
private func getDataFromBundle(_ filename: String) -> Data? {
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
return try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
}
func load<T: Decodable>(_ filename: String) -> T {
var data: Data?
getDataFromFileURL(filename: filename) { result in
switch result {
case .success(let _data):
print("4.a success loading data from getDataFromFileURL")
data = _data
case .failure(_):
print("4.b failed to load data from getDataFromFileURL")
data = getDataFromBundle(filename)
}
}
if data == nil {
print("5. after getDataFromFileURL is still nil, loading \(filename) from bundle")
data = getDataFromBundle(filename)
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data!)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}