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 */; };
E25AAC96283E14DF006E9E7F /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC95283E14DF006E9E7F /* Database.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 */; };
E2EA00C5283EA72000F7B269 /* SortCriteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C4283EA72000F7B269 /* SortCriteria.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>"; };
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>"; };
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>"; };
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>"; };
@ -100,6 +102,10 @@
E2ED70992A73D86F00067808 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
E29E17C32D4D0A9300E0EE54 /* Download */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Download; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
E25AAC75283D855D006E9E7F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@ -133,6 +139,7 @@
E25AAC7A283D855D006E9E7F /* Caps */ = {
isa = PBXGroup;
children = (
E29E17C32D4D0A9300E0EE54 /* Download */,
E25AAC7B283D855D006E9E7F /* CapsApp.swift */,
E25AAC7D283D855D006E9E7F /* ContentView.swift */,
E2EA00CF283EDD2C00F7B269 /* Camera */,
@ -149,6 +156,7 @@
E25AAC81283D855F006E9E7F /* Preview Content */ = {
isa = PBXGroup;
children = (
E29E17C82D4D0ABF00E0EE54 /* Database+Mock.swift */,
E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */,
);
path = "Preview Content";
@ -234,6 +242,9 @@
);
dependencies = (
);
fileSystemSynchronizedGroups = (
E29E17C32D4D0A9300E0EE54 /* Download */,
);
name = Caps;
packageProductDependencies = (
E2EA00C2283E672A00F7B269 /* SFSafeSymbols */,
@ -332,6 +343,7 @@
E25AAC8D283D86CF006E9E7F /* Logger.swift in Sources */,
E2ED709A2A73D86F00067808 /* String+Extensions.swift in Sources */,
E20D105028574E190019BD91 /* CapImage.swift in Sources */,
E29E17C92D4D0ABF00E0EE54 /* Database+Mock.swift in Sources */,
E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */,
E20D105228589AAC0019BD91 /* FileManager+Extensions.swift in Sources */,
882C955E2AE7F0DE00657886 /* ClassifierDownloadView.swift in Sources */,

View File

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

View File

@ -3,6 +3,7 @@ import SwiftUI
import Vision
import CryptoKit
@MainActor
final class Database: ObservableObject {
@AppStorage("classifier")
@ -33,6 +34,8 @@ final class Database: ObservableObject {
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
private let progressObserver = ClassifierProgressObserver()
@AppStorage("serverUrl")
var serverPath: String = "" {
didSet {
@ -150,9 +153,7 @@ final class Database: ObservableObject {
}
set {
_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)
}
convenience init(caps: [Int : Cap]) {
self.init()
self.caps = caps
}
func mainImage(for cap: Int) -> Int {
caps[cap]?.mainImage ?? 0
}
@ -358,9 +364,7 @@ final class Database: ObservableObject {
} else {
oldCap.update(with: cap)
let save = oldCap
DispatchQueue.main.async {
self.caps[cap.id] = save
}
self.caps[cap.id] = save
updates += 1
}
}
@ -394,9 +398,7 @@ final class Database: ObservableObject {
log("Classifier version has an invalid value '\(string)'")
return false
}
DispatchQueue.main.async {
self.storedServerClassifierVersion = serverVersion
}
self.storedServerClassifierVersion = serverVersion
return true
}
@ -409,19 +411,16 @@ final class Database: ObservableObject {
log("Downloading classifier")
let progress = ClassifierProgress()
DispatchQueue.main.async {
self.classifierDownloadProgress = progress
}
self.classifierDownloadProgress = progress
defer {
DispatchQueue.main.async {
self.classifierDownloadProgress = nil
}
self.classifierDownloadProgress = nil
}
let tempUrl: URL
let response: URLResponse
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 {
log("Failed to download classifier version: \(error)")
return false
@ -439,14 +438,14 @@ final class Database: ObservableObject {
log("Failed to replace classifier: \(error)")
return false
}
DispatchQueue.main.async {
self.storedLocalClassifierVersion = self.serverClassifierVersion
log("Downloaded classifier \(self.localClassifierVersion)")
self.classifier = nil
}
self.storedLocalClassifierVersion = self.serverClassifierVersion
log("Downloaded classifier \(self.localClassifierVersion)")
self.classifier = nil
return true
}
@discardableResult
func downloadClassifierClasses() async -> Bool {
guard let serverClassifierClassesUrl else {
@ -509,9 +508,7 @@ final class Database: ObservableObject {
func save(newCap name: String) -> Cap {
let cap = Cap(id: nextCapId, name: name)
caps[cap.id] = cap
DispatchQueue.main.async {
self.changedCaps.insert(cap.id)
}
self.changedCaps.insert(cap.id)
return cap
}
@ -526,17 +523,11 @@ final class Database: ObservableObject {
}
log("Saved image \(cap.imageCount) for cap \(capId)")
if imageUploads[capId] != nil {
DispatchQueue.main.async {
self.imageUploads[capId]!.append(cap.imageCount)
}
self.imageUploads[capId]!.append(cap.imageCount)
} else {
DispatchQueue.main.async {
self.imageUploads[capId] = [cap.imageCount]
}
}
DispatchQueue.main.async {
self.caps[capId]!.imageCount += 1
self.imageUploads[capId] = [cap.imageCount]
}
self.caps[capId]!.imageCount += 1
return true
}
@ -558,14 +549,10 @@ final class Database: ObservableObject {
return
}
log("Starting upload timer")
DispatchQueue.main.async {
self.uploadTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: self.uploadTimerElapsed)
}
}
private func uploadTimerElapsed(timer: Timer) {
Task {
await uploadAll()
self.uploadTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
Task {
await self.uploadAll()
}
}
}
@ -574,22 +561,16 @@ final class Database: ObservableObject {
log("Already uploading")
return
}
DispatchQueue.main.async {
self.isUploading = true
}
self.isUploading = true
defer {
DispatchQueue.main.async {
self.isUploading = false
}
self.isUploading = false
}
guard !changedCaps.isEmpty || pendingImageUploadCount > 0 else {
return
}
log("Starting uploads")
let uploaded = await uploadAllChangedCaps()
DispatchQueue.main.async {
self.changedCaps.subtract(uploaded)
}
self.changedCaps.subtract(uploaded)
await uploadAllImages()
log("Uploads finished")
}
@ -622,14 +603,10 @@ final class Database: ObservableObject {
}
log("Uploaded image \(image) for cap \(cap)")
let remaining = imageUploads[cap]?.filter { $0 != image }
if let r = remaining, !r.isEmpty {
DispatchQueue.main.async {
self.imageUploads[cap] = r
}
if let remaining, !remaining.isEmpty {
self.imageUploads[cap] = remaining
} else {
DispatchQueue.main.async {
self.imageUploads[cap] = nil
}
self.imageUploads[cap] = nil
}
}
}
@ -664,9 +641,7 @@ final class Database: ObservableObject {
if httpResponse.statusCode == 410 {
log("Missing cap for image \(url.lastPathComponent), reupload cap")
// Missing cap, force upload
DispatchQueue.main.async {
self.changedCaps.insert(cap)
}
self.changedCaps.insert(cap)
} else {
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)")
return false
}
DispatchQueue.main.async {
self.changedCaps.remove(cap.id)
}
self.changedCaps.remove(cap.id)
return true
} catch {
log("Failed to upload cap \(cap.id): \(error)")
@ -761,10 +734,8 @@ final class Database: ObservableObject {
}
cap.mainImage = version
let finalCap = cap
DispatchQueue.main.async {
self.caps[capId] = finalCap
log("Set main image \(version) for \(capId)")
}
self.caps[capId] = finalCap
log("Set main image \(version) for \(capId)")
return finalCap
}
@ -852,9 +823,7 @@ final class Database: ObservableObject {
// Delete cached images
images.removeCachedImages(for: cap)
// Delete cap
DispatchQueue.main.async {
self.caps[cap] = nil
}
self.caps[cap] = nil
log("Deleted cap \(cap)")
return true
} catch {
@ -888,19 +857,24 @@ final class Database: ObservableObject {
log("Image removed")
return
}
DispatchQueue.global().async {
Task {
guard let classifier = self.getClassifier() else {
return
}
log("Image classification started")
classifier.recognize(image: image) { matches in
DispatchQueue.main.async {
self.matches = matches ?? [:]
let matches = await withCheckedContinuation { continuation in
classifier.recognize(image: image) { matches in
continuation.resume(returning: matches)
}
}
self.update(matches: matches ?? [:])
}
}
func update(matches: [Int : Float]) {
self.matches = matches
}
func canClassify(cap id: Int) -> Bool {
classifierClasses.contains(id)
}
@ -1027,68 +1001,13 @@ final class Database: ObservableObject {
}
}
extension Database {
final class ClassifierProgress: NSObject, ObservableObject {
@Published
var bytesLoaded: Double
@Published
var total: Double
var percentage: Double {
guard total > 0 else {
return 0.0
extension Database: ClassifierProgressDelegate {
nonisolated func classifierProgress(_ progress: ClassifierProgress) {
Task {
await MainActor.run {
self.classifierDownloadProgress = progress
}
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,9 +2,8 @@ import SwiftUI
struct ClassifierDownloadView: View {
@ObservedObject
var progress: Database.ClassifierProgress
let progress: ClassifierProgress
var body: some View {
VStack {
ProgressView("Downloading classifier...", value: progress.bytesLoaded, total: Double(progress.total))
@ -19,5 +18,8 @@ struct ClassifierDownloadView: View {
}
#Preview {
ClassifierDownloadView(progress: .init(bytesLoaded: 12_300_000, total: 24_500_000))
ClassifierDownloadView(progress: .init(
bytesLoaded: 12_300_000,
total: 24_500_000)
)
}