From 992a367f17e4a2407b96079a5d21085e727185aa Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Sat, 11 Jul 2026 10:52:08 -0700 Subject: [PATCH] [Performance] Lazily initialize SwiftUI image state --- AsyncImageView/AsyncSwiftUIImageView.swift | 94 +++++++++++++-- .../AsyncSwiftUIImageViewStateSpec.swift | 110 ++++++++++++++++++ 2 files changed, 193 insertions(+), 11 deletions(-) create mode 100644 AsyncImageViewTests/AsyncSwiftUIImageViewStateSpec.swift diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index 4f17e2b..ea00bdc 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -20,23 +20,82 @@ public struct AsyncSwiftUIImageView< Renderer.Error == Never, PlaceholderRenderer.Data == Data, PlaceholderRenderer.Error == Never, -Renderer.RenderResult == PlaceholderRenderer.RenderResult { + Renderer.RenderResult == PlaceholderRenderer.RenderResult { private typealias ViewModel = AsyncSwiftUIImageViewModel - @State private var viewModel: ViewModel + @State private var viewModelReference: LazyReference + + private var viewModel: ViewModel { + self.viewModelReference.value + } + + public init( + renderer: Renderer, + placeholderRenderer: PlaceholderRenderer? = nil + ) { + self.init( + renderer: renderer, + placeholderRenderer: placeholderRenderer, + uiSchedulerFactory: { UIScheduler() }, + imageCreationSchedulerFactory: { QueueScheduler() } + ) + } + + public init( + renderer: Renderer, + placeholderRenderer: PlaceholderRenderer? = nil, + uiScheduler: ReactiveSwift.Scheduler + ) { + self.init( + renderer: renderer, + placeholderRenderer: placeholderRenderer, + uiSchedulerFactory: { uiScheduler }, + imageCreationSchedulerFactory: { QueueScheduler() } + ) + } + + public init( + renderer: Renderer, + placeholderRenderer: PlaceholderRenderer? = nil, + imageCreationScheduler: ReactiveSwift.Scheduler + ) { + self.init( + renderer: renderer, + placeholderRenderer: placeholderRenderer, + uiSchedulerFactory: { UIScheduler() }, + imageCreationSchedulerFactory: { imageCreationScheduler } + ) + } public init( renderer: Renderer, placeholderRenderer: PlaceholderRenderer? = nil, - uiScheduler: ReactiveSwift.Scheduler = UIScheduler(), - imageCreationScheduler: ReactiveSwift.Scheduler = QueueScheduler()) { - _viewModel = State( - initialValue: ViewModel( - renderer: renderer, - placeholderRenderer: placeholderRenderer, - uiScheduler: uiScheduler, - imageCreationScheduler: imageCreationScheduler - ) + uiScheduler: ReactiveSwift.Scheduler, + imageCreationScheduler: ReactiveSwift.Scheduler + ) { + self.init( + renderer: renderer, + placeholderRenderer: placeholderRenderer, + uiSchedulerFactory: { uiScheduler }, + imageCreationSchedulerFactory: { imageCreationScheduler } + ) + } + + internal init( + renderer: Renderer, + placeholderRenderer: PlaceholderRenderer?, + uiSchedulerFactory: @escaping () -> ReactiveSwift.Scheduler, + imageCreationSchedulerFactory: @escaping () -> ReactiveSwift.Scheduler + ) { + _viewModelReference = State( + initialValue: LazyReference { + ViewModel( + renderer: renderer, + placeholderRenderer: placeholderRenderer, + uiScheduler: uiSchedulerFactory(), + imageCreationScheduler: imageCreationSchedulerFactory() + ) + } ) } @@ -101,6 +160,19 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { } } +private final class LazyReference { + private let factory: () -> Value + private lazy var storedValue = self.factory() + + var value: Value { + self.storedValue + } + + init(factory: @escaping () -> Value) { + self.factory = factory + } +} + public extension AsyncSwiftUIImageView { func data(_ data: ImageViewData) -> Self { var view = self diff --git a/AsyncImageViewTests/AsyncSwiftUIImageViewStateSpec.swift b/AsyncImageViewTests/AsyncSwiftUIImageViewStateSpec.swift new file mode 100644 index 0000000..90d54d1 --- /dev/null +++ b/AsyncImageViewTests/AsyncSwiftUIImageViewStateSpec.swift @@ -0,0 +1,110 @@ +import Quick +import Nimble +import SwiftUI +import UIKit + +import ReactiveSwift + +@testable import AsyncImageView + +class AsyncSwiftUIImageViewStateSpec: QuickSpec { + override class func spec() { + describe("AsyncSwiftUIImageView state") { + it("preserves every scheduler initializer combination") { + let renderer = StateTestRenderer() + + _ = StateTestImageView(renderer: renderer) + _ = StateTestImageView(renderer: renderer, uiScheduler: ImmediateScheduler()) + _ = StateTestImageView(renderer: renderer, imageCreationScheduler: ImmediateScheduler()) + _ = StateTestImageView( + renderer: renderer, + uiScheduler: ImmediateScheduler(), + imageCreationScheduler: ImmediateScheduler() + ) + } + + it("creates its retained schedulers only once across parent updates") { + let schedulerFactory = SchedulerFactory() + let renderer = StateTestRenderer() + let initialView = StateTestContainer( + generation: 0, + renderer: renderer, + schedulerFactory: schedulerFactory + ) + let viewController = UIHostingController(rootView: initialView) + let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 100))) + window.rootViewController = viewController + window.makeKeyAndVisible() + viewController.view.frame = window.bounds + viewController.view.layoutIfNeeded() + + for generation in 1...100 { + viewController.rootView = StateTestContainer( + generation: generation, + renderer: renderer, + schedulerFactory: schedulerFactory + ) + viewController.view.layoutIfNeeded() + } + + expect(schedulerFactory.uiSchedulerCount.value) == 1 + expect(schedulerFactory.imageSchedulerCount.value) == 1 + } + } + } +} + +private struct StateTestContainer: View { + let generation: Int + let renderer: StateTestRenderer + let schedulerFactory: SchedulerFactory + + var body: some View { + StateTestImageView( + renderer: self.renderer, + placeholderRenderer: nil, + uiSchedulerFactory: self.schedulerFactory.makeUIScheduler, + imageCreationSchedulerFactory: self.schedulerFactory.makeImageScheduler + ) + .frame(width: 100, height: 100) + .accessibilityIdentifier("generation-\(self.generation)") + } +} + +private typealias StateTestImageView = AsyncSwiftUIImageView< + StateTestRenderData, + StateTestViewData, + StateTestRenderer, + StateTestRenderer +> + +private final class SchedulerFactory { + let uiSchedulerCount = Atomic(0) + let imageSchedulerCount = Atomic(0) + + func makeUIScheduler() -> ReactiveSwift.Scheduler { + self.uiSchedulerCount.modify { $0 += 1 } + return ImmediateScheduler() + } + + func makeImageScheduler() -> ReactiveSwift.Scheduler { + self.imageSchedulerCount.modify { $0 += 1 } + return ImmediateScheduler() + } +} + +private struct StateTestViewData: ImageViewDataType { + func renderDataWithSize(_ size: CGSize) -> StateTestRenderData { + StateTestRenderData(size: size) + } +} + +private struct StateTestRenderData: RenderDataType { + let size: CGSize +} + +private final class StateTestRenderer: RendererType { + func renderImageWithData(_ data: StateTestRenderData) -> SignalProducer { + SignalProducer(value: UIImage()) + } +}