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

View file

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

View file

@ -7,6 +7,7 @@ struct HomePosterCell: View {
var item: Components.Schemas.BaseItemDto
@Injected(\.client) var client
@Injected(\.pageAnimationTracker) var pageAnimationTracker
@State private var imageData: Data?
var view: Body {
@ -47,7 +48,9 @@ struct HomePosterCell: View {
.padding(12, .horizontal)
}
.onAppear {
loadImage()
Idle {
loadImage()
}
}
.overflow(.hidden)
.card()
@ -67,7 +70,13 @@ struct HomePosterCell: View {
)
else { return }
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 Foundation
import LuminateCore
import LuminateDI
struct PosterCell: View {
var item: Components.Schemas.BaseItemDto
var client: JellyfinClient
@Injected(\.pageAnimationTracker) var pageAnimationTracker
@State private var imageData: Data?
var view: Body {
@ -27,7 +29,9 @@ struct PosterCell: View {
.frame(maxWidth: 150)
}
.onAppear {
loadImage()
Idle {
loadImage()
}
}
}
@ -44,7 +48,13 @@ struct PosterCell: View {
)
guard let url else { return }
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 Foundation
import LuminateCore
import LuminateDI
struct SearchView: View {
@ -59,6 +60,7 @@ struct SearchResultRow: View {
var hint: Components.Schemas.SearchHint
var client: JellyfinClient
@Injected(\.pageAnimationTracker) var pageAnimationTracker
@State private var imageData: Data?
var view: Body {
@ -99,7 +101,9 @@ struct SearchResultRow: View {
}
.padding(5, .vertical)
.onAppear {
loadImage()
Idle {
loadImage()
}
}
}
@ -114,7 +118,13 @@ struct SearchResultRow: View {
)
else { return }
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
}
}
}