Improve progress updating, fix warnings

This commit is contained in:
Christoph Hagen 2025-01-31 14:52:20 +01:00
parent 3dc5674a3a
commit 92e1eacf57
9 changed files with 154 additions and 144 deletions

View File

@ -29,6 +29,7 @@
E25AAC94283D88A4006E9E7F /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC93283D88A4006E9E7F /* Cap.swift */; }; E25AAC94283D88A4006E9E7F /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC93283D88A4006E9E7F /* Cap.swift */; };
E25AAC96283E14DF006E9E7F /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC95283E14DF006E9E7F /* Database.swift */; }; E25AAC96283E14DF006E9E7F /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC95283E14DF006E9E7F /* Database.swift */; };
E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC9A283E3395006E9E7F /* CapRowView.swift */; }; E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC9A283E3395006E9E7F /* CapRowView.swift */; };
E29E17C92D4D0ABF00E0EE54 /* Database+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29E17C82D4D0ABF00E0EE54 /* Database+Mock.swift */; };
E2EA00C3283E672A00F7B269 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2EA00C2283E672A00F7B269 /* SFSafeSymbols */; }; E2EA00C3283E672A00F7B269 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2EA00C2283E672A00F7B269 /* SFSafeSymbols */; };
E2EA00C5283EA72000F7B269 /* SortCriteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C4283EA72000F7B269 /* SortCriteria.swift */; }; E2EA00C5283EA72000F7B269 /* SortCriteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C4283EA72000F7B269 /* SortCriteria.swift */; };
E2EA00C7283EAA0100F7B269 /* SortSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */; }; E2EA00C7283EAA0100F7B269 /* SortSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */; };
@ -78,6 +79,7 @@
E25AAC95283E14DF006E9E7F /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; }; E25AAC95283E14DF006E9E7F /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
E25AAC9A283E3395006E9E7F /* CapRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapRowView.swift; sourceTree = "<group>"; }; E25AAC9A283E3395006E9E7F /* CapRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapRowView.swift; sourceTree = "<group>"; };
E268C72D29BF2D8400D813A0 /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = "<group>"; }; E268C72D29BF2D8400D813A0 /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = "<group>"; };
E29E17C82D4D0ABF00E0EE54 /* Database+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Database+Mock.swift"; sourceTree = "<group>"; };
E2EA00C4283EA72000F7B269 /* SortCriteria.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCriteria.swift; sourceTree = "<group>"; }; E2EA00C4283EA72000F7B269 /* SortCriteria.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCriteria.swift; sourceTree = "<group>"; };
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortSelectionView.swift; sourceTree = "<group>"; }; E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortSelectionView.swift; sourceTree = "<group>"; };
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCaseRowView.swift; sourceTree = "<group>"; }; E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCaseRowView.swift; sourceTree = "<group>"; };
@ -100,6 +102,10 @@
E2ED70992A73D86F00067808 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; }; E2ED70992A73D86F00067808 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
E29E17C32D4D0A9300E0EE54 /* Download */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Download; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
E25AAC75283D855D006E9E7F /* Frameworks */ = { E25AAC75283D855D006E9E7F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
@ -133,6 +139,7 @@
E25AAC7A283D855D006E9E7F /* Caps */ = { E25AAC7A283D855D006E9E7F /* Caps */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E29E17C32D4D0A9300E0EE54 /* Download */,
E25AAC7B283D855D006E9E7F /* CapsApp.swift */, E25AAC7B283D855D006E9E7F /* CapsApp.swift */,
E25AAC7D283D855D006E9E7F /* ContentView.swift */, E25AAC7D283D855D006E9E7F /* ContentView.swift */,
E2EA00CF283EDD2C00F7B269 /* Camera */, E2EA00CF283EDD2C00F7B269 /* Camera */,
@ -149,6 +156,7 @@
E25AAC81283D855F006E9E7F /* Preview Content */ = { E25AAC81283D855F006E9E7F /* Preview Content */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E29E17C82D4D0ABF00E0EE54 /* Database+Mock.swift */,
E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */, E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */,
); );
path = "Preview Content"; path = "Preview Content";
@ -234,6 +242,9 @@
); );
dependencies = ( dependencies = (
); );
fileSystemSynchronizedGroups = (
E29E17C32D4D0A9300E0EE54 /* Download */,
);
name = Caps; name = Caps;
packageProductDependencies = ( packageProductDependencies = (
E2EA00C2283E672A00F7B269 /* SFSafeSymbols */, E2EA00C2283E672A00F7B269 /* SFSafeSymbols */,
@ -332,6 +343,7 @@
E25AAC8D283D86CF006E9E7F /* Logger.swift in Sources */, E25AAC8D283D86CF006E9E7F /* Logger.swift in Sources */,
E2ED709A2A73D86F00067808 /* String+Extensions.swift in Sources */, E2ED709A2A73D86F00067808 /* String+Extensions.swift in Sources */,
E20D105028574E190019BD91 /* CapImage.swift in Sources */, E20D105028574E190019BD91 /* CapImage.swift in Sources */,
E29E17C92D4D0ABF00E0EE54 /* Database+Mock.swift in Sources */,
E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */, E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */,
E20D105228589AAC0019BD91 /* FileManager+Extensions.swift in Sources */, E20D105228589AAC0019BD91 /* FileManager+Extensions.swift in Sources */,
882C955E2AE7F0DE00657886 /* ClassifierDownloadView.swift in Sources */, 882C955E2AE7F0DE00657886 /* ClassifierDownloadView.swift in Sources */,

View File

@ -1,12 +1,13 @@
{ {
"originHash" : "c78617470808606280f24ff566c34cb1b032779babfb39f1ddbcda602b363bd4",
"pins" : [ "pins" : [
{ {
"identity" : "bottom-sheet", "identity" : "bottom-sheet",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/weitieda/bottom-sheet", "location" : "https://github.com/weitieda/bottom-sheet",
"state" : { "state" : {
"revision" : "4e074d49f3148577ac66cf47b85a99d016480d01", "revision" : "6b21007153365235418f3943a960a1f9546592e0",
"version" : "1.0.10" "version" : "1.0.12"
} }
}, },
{ {
@ -19,5 +20,5 @@
} }
} }
], ],
"version" : 2 "version" : 3
} }

View File

@ -3,6 +3,7 @@ import SwiftUI
import Vision import Vision
import CryptoKit import CryptoKit
@MainActor
final class Database: ObservableObject { final class Database: ObservableObject {
@AppStorage("classifier") @AppStorage("classifier")
@ -33,6 +34,8 @@ final class Database: ObservableObject {
private let encoder = JSONEncoder() private let encoder = JSONEncoder()
private let decoder = JSONDecoder() private let decoder = JSONDecoder()
private let progressObserver = ClassifierProgressObserver()
@AppStorage("serverUrl") @AppStorage("serverUrl")
var serverPath: String = "" { var serverPath: String = "" {
didSet { didSet {
@ -150,9 +153,7 @@ final class Database: ObservableObject {
} }
set { set {
_classifierClassesCache = newValue _classifierClassesCache = newValue
DispatchQueue.main.async { self._classifierClassesString = newValue.map { "\($0)" }.joined(separator: ",")
self._classifierClassesString = newValue.map { "\($0)" }.joined(separator: ",")
}
} }
} }
@ -187,6 +188,11 @@ final class Database: ObservableObject {
updatePendingImageUploadCount(imageUploads: imageUploads) updatePendingImageUploadCount(imageUploads: imageUploads)
} }
convenience init(caps: [Int : Cap]) {
self.init()
self.caps = caps
}
func mainImage(for cap: Int) -> Int { func mainImage(for cap: Int) -> Int {
caps[cap]?.mainImage ?? 0 caps[cap]?.mainImage ?? 0
} }
@ -358,9 +364,7 @@ final class Database: ObservableObject {
} else { } else {
oldCap.update(with: cap) oldCap.update(with: cap)
let save = oldCap let save = oldCap
DispatchQueue.main.async { self.caps[cap.id] = save
self.caps[cap.id] = save
}
updates += 1 updates += 1
} }
} }
@ -394,9 +398,7 @@ final class Database: ObservableObject {
log("Classifier version has an invalid value '\(string)'") log("Classifier version has an invalid value '\(string)'")
return false return false
} }
DispatchQueue.main.async { self.storedServerClassifierVersion = serverVersion
self.storedServerClassifierVersion = serverVersion
}
return true return true
} }
@ -409,19 +411,16 @@ final class Database: ObservableObject {
log("Downloading classifier") log("Downloading classifier")
let progress = ClassifierProgress() let progress = ClassifierProgress()
DispatchQueue.main.async { self.classifierDownloadProgress = progress
self.classifierDownloadProgress = progress
}
defer { defer {
DispatchQueue.main.async { self.classifierDownloadProgress = nil
self.classifierDownloadProgress = nil
}
} }
let tempUrl: URL let tempUrl: URL
let response: URLResponse let response: URLResponse
do { do {
(tempUrl, response) = try await URLSession.shared.download(from: serverClassifierUrl, delegate: progress) progressObserver.delegate = self
(tempUrl, response) = try await URLSession.shared.download(from: serverClassifierUrl, delegate: progressObserver)
} catch { } catch {
log("Failed to download classifier version: \(error)") log("Failed to download classifier version: \(error)")
return false return false
@ -439,11 +438,11 @@ final class Database: ObservableObject {
log("Failed to replace classifier: \(error)") log("Failed to replace classifier: \(error)")
return false return false
} }
DispatchQueue.main.async {
self.storedLocalClassifierVersion = self.serverClassifierVersion self.storedLocalClassifierVersion = self.serverClassifierVersion
log("Downloaded classifier \(self.localClassifierVersion)") log("Downloaded classifier \(self.localClassifierVersion)")
self.classifier = nil self.classifier = nil
}
return true return true
} }
@ -509,9 +508,7 @@ final class Database: ObservableObject {
func save(newCap name: String) -> Cap { func save(newCap name: String) -> Cap {
let cap = Cap(id: nextCapId, name: name) let cap = Cap(id: nextCapId, name: name)
caps[cap.id] = cap caps[cap.id] = cap
DispatchQueue.main.async { self.changedCaps.insert(cap.id)
self.changedCaps.insert(cap.id)
}
return cap return cap
} }
@ -526,17 +523,11 @@ final class Database: ObservableObject {
} }
log("Saved image \(cap.imageCount) for cap \(capId)") log("Saved image \(cap.imageCount) for cap \(capId)")
if imageUploads[capId] != nil { if imageUploads[capId] != nil {
DispatchQueue.main.async { self.imageUploads[capId]!.append(cap.imageCount)
self.imageUploads[capId]!.append(cap.imageCount)
}
} else { } else {
DispatchQueue.main.async { self.imageUploads[capId] = [cap.imageCount]
self.imageUploads[capId] = [cap.imageCount]
}
}
DispatchQueue.main.async {
self.caps[capId]!.imageCount += 1
} }
self.caps[capId]!.imageCount += 1
return true return true
} }
@ -558,14 +549,10 @@ final class Database: ObservableObject {
return return
} }
log("Starting upload timer") log("Starting upload timer")
DispatchQueue.main.async { self.uploadTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
self.uploadTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: self.uploadTimerElapsed) Task {
} await self.uploadAll()
} }
private func uploadTimerElapsed(timer: Timer) {
Task {
await uploadAll()
} }
} }
@ -574,22 +561,16 @@ final class Database: ObservableObject {
log("Already uploading") log("Already uploading")
return return
} }
DispatchQueue.main.async { self.isUploading = true
self.isUploading = true
}
defer { defer {
DispatchQueue.main.async { self.isUploading = false
self.isUploading = false
}
} }
guard !changedCaps.isEmpty || pendingImageUploadCount > 0 else { guard !changedCaps.isEmpty || pendingImageUploadCount > 0 else {
return return
} }
log("Starting uploads") log("Starting uploads")
let uploaded = await uploadAllChangedCaps() let uploaded = await uploadAllChangedCaps()
DispatchQueue.main.async { self.changedCaps.subtract(uploaded)
self.changedCaps.subtract(uploaded)
}
await uploadAllImages() await uploadAllImages()
log("Uploads finished") log("Uploads finished")
} }
@ -622,14 +603,10 @@ final class Database: ObservableObject {
} }
log("Uploaded image \(image) for cap \(cap)") log("Uploaded image \(image) for cap \(cap)")
let remaining = imageUploads[cap]?.filter { $0 != image } let remaining = imageUploads[cap]?.filter { $0 != image }
if let r = remaining, !r.isEmpty { if let remaining, !remaining.isEmpty {
DispatchQueue.main.async { self.imageUploads[cap] = remaining
self.imageUploads[cap] = r
}
} else { } else {
DispatchQueue.main.async { self.imageUploads[cap] = nil
self.imageUploads[cap] = nil
}
} }
} }
} }
@ -664,9 +641,7 @@ final class Database: ObservableObject {
if httpResponse.statusCode == 410 { if httpResponse.statusCode == 410 {
log("Missing cap for image \(url.lastPathComponent), reupload cap") log("Missing cap for image \(url.lastPathComponent), reupload cap")
// Missing cap, force upload // Missing cap, force upload
DispatchQueue.main.async { self.changedCaps.insert(cap)
self.changedCaps.insert(cap)
}
} else { } else {
log("Failed to upload image \(url.lastPathComponent): Response \(httpResponse.statusCode)") log("Failed to upload image \(url.lastPathComponent): Response \(httpResponse.statusCode)")
} }
@ -736,9 +711,7 @@ final class Database: ObservableObject {
log("Failed to upload cap \(cap.id): Response \(httpResponse.statusCode)") log("Failed to upload cap \(cap.id): Response \(httpResponse.statusCode)")
return false return false
} }
DispatchQueue.main.async { self.changedCaps.remove(cap.id)
self.changedCaps.remove(cap.id)
}
return true return true
} catch { } catch {
log("Failed to upload cap \(cap.id): \(error)") log("Failed to upload cap \(cap.id): \(error)")
@ -761,10 +734,8 @@ final class Database: ObservableObject {
} }
cap.mainImage = version cap.mainImage = version
let finalCap = cap let finalCap = cap
DispatchQueue.main.async { self.caps[capId] = finalCap
self.caps[capId] = finalCap log("Set main image \(version) for \(capId)")
log("Set main image \(version) for \(capId)")
}
return finalCap return finalCap
} }
@ -852,9 +823,7 @@ final class Database: ObservableObject {
// Delete cached images // Delete cached images
images.removeCachedImages(for: cap) images.removeCachedImages(for: cap)
// Delete cap // Delete cap
DispatchQueue.main.async { self.caps[cap] = nil
self.caps[cap] = nil
}
log("Deleted cap \(cap)") log("Deleted cap \(cap)")
return true return true
} catch { } catch {
@ -888,19 +857,24 @@ final class Database: ObservableObject {
log("Image removed") log("Image removed")
return return
} }
DispatchQueue.global().async { Task {
guard let classifier = self.getClassifier() else { guard let classifier = self.getClassifier() else {
return return
} }
log("Image classification started") log("Image classification started")
classifier.recognize(image: image) { matches in let matches = await withCheckedContinuation { continuation in
DispatchQueue.main.async { classifier.recognize(image: image) { matches in
self.matches = matches ?? [:] continuation.resume(returning: matches)
} }
} }
self.update(matches: matches ?? [:])
} }
} }
func update(matches: [Int : Float]) {
self.matches = matches
}
func canClassify(cap id: Int) -> Bool { func canClassify(cap id: Int) -> Bool {
classifierClasses.contains(id) classifierClasses.contains(id)
} }
@ -1027,68 +1001,13 @@ final class Database: ObservableObject {
} }
} }
extension Database { extension Database: ClassifierProgressDelegate {
final class ClassifierProgress: NSObject, ObservableObject { nonisolated func classifierProgress(_ progress: ClassifierProgress) {
Task {
@Published await MainActor.run {
var bytesLoaded: Double self.classifierDownloadProgress = progress
@Published
var total: Double
var percentage: Double {
guard total > 0 else {
return 0.0
} }
return bytesLoaded * 100 / total
}
init(bytesLoaded: Double = 0, total: Double = 0) {
self.bytesLoaded = bytesLoaded
self.total = total
} }
} }
} }
extension Database.ClassifierProgress: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
DispatchQueue.main.async {
self.bytesLoaded = Double(totalBytesWritten)
self.total = Double(totalBytesExpectedToWrite)
}
}
}
extension Database {
static var mock: Database {
let db = Database()
db.serverPath = "https://caps.christophhagen.de"
db.caps = [
Cap(id: 123, name: "My new cap"),
Cap(id: 234, name: "My favorite cap"),
Cap(id: 345, name: "My oldest cap"),
Cap(id: 456, name: "My new cap"),
Cap(id: 567, name: "My favorite cap"),
Cap(id: 678, name: "My oldest cap"),
].reduce(into: [:]) { $0[$1.id] = $1 }
db.image = UIImage(systemSymbol: .photo)
return db
}
static var largeMock: Database {
let db = Database()
db.serverPath = "https://caps.christophhagen.de"
db.caps = (1..<500)
.map { Cap(id: $0, name: "Cap \($0)") }
.reduce(into: [:]) { $0[$1.id] = $1 }
db.image = UIImage(systemSymbol: .photo)
return db
}
}

View File

@ -0,0 +1,19 @@
struct ClassifierProgress {
let bytesLoaded: Double
let total: Double
var percentage: Double {
guard total > 0 else {
return 0.0
}
return bytesLoaded * 100 / total
}
init(bytesLoaded: Double = 0, total: Double = 0) {
self.bytesLoaded = bytesLoaded
self.total = total
}
}

View File

@ -0,0 +1,5 @@
protocol ClassifierProgressDelegate: AnyObject {
func classifierProgress(_ progress: ClassifierProgress)
}

View File

@ -0,0 +1,20 @@
import Foundation
final class ClassifierProgressObserver: NSObject {
weak var delegate: ClassifierProgressDelegate?
}
extension ClassifierProgressObserver: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
delegate?.classifierProgress(.init(
bytesLoaded: Double(totalBytesWritten),
total: Double(totalBytesExpectedToWrite)))
}
}

View File

@ -0,0 +1,32 @@
import UIKit
extension Database {
static var mock: Database {
let caps = [
Cap(id: 123, name: "My new cap"),
Cap(id: 234, name: "My favorite cap"),
Cap(id: 345, name: "My oldest cap"),
Cap(id: 456, name: "My new cap"),
Cap(id: 567, name: "My favorite cap"),
Cap(id: 678, name: "My oldest cap"),
].reduce(into: [:]) { $0[$1.id] = $1 }
let db = Database(caps: caps)
db.serverPath = "https://caps.christophhagen.de"
db.image = UIImage(systemSymbol: .photo)
return db
}
static var largeMock: Database {
let caps = (1..<500)
.map { Cap(id: $0, name: "Cap \($0)") }
.reduce(into: [:]) { $0[$1.id] = $1 }
let db = Database(caps: caps)
db.serverPath = "https://caps.christophhagen.de"
db.image = UIImage(systemSymbol: .photo)
return db
}
}

View File

@ -2,8 +2,7 @@ import SwiftUI
struct ClassifierDownloadView: View { struct ClassifierDownloadView: View {
@ObservedObject let progress: ClassifierProgress
var progress: Database.ClassifierProgress
var body: some View { var body: some View {
VStack { VStack {
@ -19,5 +18,8 @@ struct ClassifierDownloadView: View {
} }
#Preview { #Preview {
ClassifierDownloadView(progress: .init(bytesLoaded: 12_300_000, total: 24_500_000)) ClassifierDownloadView(progress: .init(
bytesLoaded: 12_300_000,
total: 24_500_000)
)
} }