Defer image loading during page transition animations

This commit is contained in:
Brendan Szymanski 2026-06-15 19:38:32 -04:00
parent 0c011eff01
commit cd2c435a04
8 changed files with 89 additions and 6 deletions

View file

@ -50,11 +50,21 @@ let package = Package(
.product(name: "Adwaita", package: "adwaita-swift"), .product(name: "Adwaita", package: "adwaita-swift"),
] ]
), ),
.target(
name: "LuminateUI",
dependencies: [
"LuminateCore",
"LuminateDI",
"LuminateObservationMacros",
.product(name: "Adwaita", package: "adwaita-swift"),
]
),
.target( .target(
name: "LuminateHome", name: "LuminateHome",
dependencies: [ dependencies: [
"LuminateCore", "LuminateCore",
"LuminateDI", "LuminateDI",
"LuminateUI",
"LuminateObservationMacros", "LuminateObservationMacros",
.product(name: "Adwaita", package: "adwaita-swift"), .product(name: "Adwaita", package: "adwaita-swift"),
] ]
@ -64,6 +74,7 @@ let package = Package(
dependencies: [ dependencies: [
"LuminateCore", "LuminateCore",
"LuminateDI", "LuminateDI",
"LuminateUI",
"LuminateObservationMacros", "LuminateObservationMacros",
.product(name: "Adwaita", package: "adwaita-swift"), .product(name: "Adwaita", package: "adwaita-swift"),
] ]
@ -73,6 +84,7 @@ let package = Package(
dependencies: [ dependencies: [
"LuminateCore", "LuminateCore",
"LuminateDI", "LuminateDI",
"LuminateUI",
"LuminateObservationMacros", "LuminateObservationMacros",
.product(name: "Adwaita", package: "adwaita-swift"), .product(name: "Adwaita", package: "adwaita-swift"),
] ]
@ -83,6 +95,7 @@ let package = Package(
"LuminateHome", "LuminateHome",
"LuminateLibrary", "LuminateLibrary",
"LuminatePlayer", "LuminatePlayer",
"LuminateUI",
"LuminateDI", "LuminateDI",
"LuminateObservationMacros", "LuminateObservationMacros",
.product(name: "Adwaita", package: "adwaita-swift"), .product(name: "Adwaita", package: "adwaita-swift"),

View file

@ -5,6 +5,7 @@ import LuminateDI
import LuminateHome import LuminateHome
import LuminateLibrary import LuminateLibrary
import LuminatePlayer import LuminatePlayer
import LuminateUI
@main @main
struct Luminate: App { struct Luminate: App {
@ -19,6 +20,7 @@ struct Luminate: App {
if let store = try? SQLiteStore(dbURL: SQLiteStore.defaultDatabaseURL()) { if let store = try? SQLiteStore(dbURL: SQLiteStore.defaultDatabaseURL()) {
DIContainer.shared.register(\.persistence, value: store) DIContainer.shared.register(\.persistence, value: store)
} }
DIContainer.shared.register(\.pageAnimationTracker, value: PageAnimationTracker())
} }
var scene: Scene { var scene: Scene {
@ -40,6 +42,8 @@ struct Luminate: App {
DIContainer.shared.register(\.client, value: client) DIContainer.shared.register(\.client, value: client)
DIContainer.shared.register(\.userId, value: id) DIContainer.shared.register(\.userId, value: id)
DIContainer.shared.register(\.imageService, value: ImageService()) DIContainer.shared.register(\.imageService, value: ImageService())
DIContainer.shared.register(
\.pageAnimationTracker, value: PageAnimationTracker())
self.client = client self.client = client
self.userId = id self.userId = id
@ -72,6 +76,7 @@ struct Luminate: App {
DIContainer.shared.register(\.client, value: client) DIContainer.shared.register(\.client, value: client)
DIContainer.shared.register(\.userId, value: auth.userId) DIContainer.shared.register(\.userId, value: auth.userId)
DIContainer.shared.register(\.imageService, value: ImageService()) DIContainer.shared.register(\.imageService, value: ImageService())
DIContainer.shared.register(\.pageAnimationTracker, value: PageAnimationTracker())
self.client = client self.client = client
self.userId = auth.userId self.userId = auth.userId
@ -87,6 +92,7 @@ struct ContentView: View {
var client: JellyfinClient var client: JellyfinClient
var userId: String var userId: String
@State var stack: NavigationStack<Page> = .init() @State var stack: NavigationStack<Page> = .init()
@Injected(\.pageAnimationTracker) var pageAnimationTracker
var view: Body { var view: Body {
NavigationView($stack, "Luminate") { page in NavigationView($stack, "Luminate") { page in
@ -107,5 +113,11 @@ struct ContentView: View {
} }
.navigationTitle("Luminate") .navigationTitle("Luminate")
} }
.pushed {
pageAnimationTracker.markPush()
}
.popped {
pageAnimationTracker.markPush()
}
} }
} }

View file

@ -0,0 +1,6 @@
import Foundation
public protocol PageAnimationTracking: AnyObject {
var isAnimating: Bool { get }
func markPush()
}

View file

@ -8,6 +8,7 @@ public struct InjectionValues {
public var imageService: ImageService? public var imageService: ImageService?
public var webSocketClient: WebSocketClient? public var webSocketClient: WebSocketClient?
public var persistence: PersistenceService? public var persistence: PersistenceService?
public var pageAnimationTracker: (any PageAnimationTracking)?
public init() {} public init() {}
} }

View file

@ -7,6 +7,7 @@ struct HomePosterCell: View {
var item: Components.Schemas.BaseItemDto var item: Components.Schemas.BaseItemDto
@Injected(\.client) var client @Injected(\.client) var client
@Injected(\.pageAnimationTracker) var pageAnimationTracker
@State private var imageData: Data? @State private var imageData: Data?
var view: Body { var view: Body {
@ -47,8 +48,10 @@ struct HomePosterCell: View {
.padding(12, .horizontal) .padding(12, .horizontal)
} }
.onAppear { .onAppear {
Idle {
loadImage() loadImage()
} }
}
.overflow(.hidden) .overflow(.hidden)
.card() .card()
} }
@ -67,7 +70,13 @@ struct HomePosterCell: View {
) )
else { return } else { return }
let service = ImageService() let service = ImageService()
imageData = try? await service.loadImage(url: url) let data = try? await service.loadImage(url: url)
if pageAnimationTracker.isAnimating {
_imageData.rawValue = data
} else {
imageData = data
}
} }
} }
} }

View file

@ -1,11 +1,13 @@
import Adwaita import Adwaita
import Foundation import Foundation
import LuminateCore import LuminateCore
import LuminateDI
struct PosterCell: View { struct PosterCell: View {
var item: Components.Schemas.BaseItemDto var item: Components.Schemas.BaseItemDto
var client: JellyfinClient var client: JellyfinClient
@Injected(\.pageAnimationTracker) var pageAnimationTracker
@State private var imageData: Data? @State private var imageData: Data?
var view: Body { var view: Body {
@ -27,9 +29,11 @@ struct PosterCell: View {
.frame(maxWidth: 150) .frame(maxWidth: 150)
} }
.onAppear { .onAppear {
Idle {
loadImage() loadImage()
} }
} }
}
private func loadImage() { private func loadImage() {
guard let tag = item.primaryImageTag, guard let tag = item.primaryImageTag,
@ -44,7 +48,13 @@ struct PosterCell: View {
) )
guard let url else { return } guard let url else { return }
let service = ImageService() let service = ImageService()
imageData = try? await service.loadImage(url: url) let data = try? await service.loadImage(url: url)
if pageAnimationTracker.isAnimating {
_imageData.rawValue = data
} else {
imageData = data
}
} }
} }
} }

View file

@ -1,6 +1,7 @@
import Adwaita import Adwaita
import Foundation import Foundation
import LuminateCore import LuminateCore
import LuminateDI
struct SearchView: View { struct SearchView: View {
@ -59,6 +60,7 @@ struct SearchResultRow: View {
var hint: Components.Schemas.SearchHint var hint: Components.Schemas.SearchHint
var client: JellyfinClient var client: JellyfinClient
@Injected(\.pageAnimationTracker) var pageAnimationTracker
@State private var imageData: Data? @State private var imageData: Data?
var view: Body { var view: Body {
@ -99,9 +101,11 @@ struct SearchResultRow: View {
} }
.padding(5, .vertical) .padding(5, .vertical)
.onAppear { .onAppear {
Idle {
loadImage() loadImage()
} }
} }
}
private func loadImage() { private func loadImage() {
guard let tag = hint.primaryImageTag, guard let tag = hint.primaryImageTag,
@ -114,7 +118,13 @@ struct SearchResultRow: View {
) )
else { return } else { return }
let service = ImageService() let service = ImageService()
imageData = try? await service.loadImage(url: url) let data = try? await service.loadImage(url: url)
if pageAnimationTracker.isAnimating {
_imageData.rawValue = data
} else {
imageData = data
}
} }
} }
} }

View file

@ -0,0 +1,22 @@
import Adwaita
import Foundation
import LuminateCore
public class PageAnimationTracker: PageAnimationTracking {
public var isAnimating = false
private var pushGeneration = 0
public init() {}
public func markPush() {
isAnimating = true
pushGeneration += 1
let captured = pushGeneration
Idle(delay: 250) { [weak self] in
guard let self, self.pushGeneration == captured else { return false }
self.isAnimating = false
StateManager.updateViews()
return false
}
}
}