From 46ec81c3f6ded18aa430920083f66636ef67aa3f Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Sat, 11 Jul 2026 10:52:02 -0700 Subject: [PATCH 1/2] [Performance] Streamline SwiftUI size observation --- AsyncImageView/AsyncSwiftUIImageView.swift | 30 +++---------- .../AsyncSwiftUIImageViewSpec.swift | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 AsyncImageViewTests/AsyncSwiftUIImageViewSpec.swift diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index 4f17e2b..9d68e3e 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -60,10 +60,11 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { self.imageView Color.clear - .modifier(SizeModifier()) - .onPreferenceChange(ImageSizePreferenceKey.self) { imageSize in - self.size = imageSize - } + } + .onGeometryChange(for: CGSize.self) { geometry in + geometry.size + } action: { imageSize in + self.size = imageSize } .onAppear { self.viewModel.start() @@ -110,27 +111,6 @@ public extension AsyncSwiftUIImageView { } } -private struct ImageSizePreferenceKey: PreferenceKey { - typealias Value = CGSize - - static var defaultValue: CGSize = .zero - - static func reduce(value: inout CGSize, nextValue: () -> CGSize) { - value = nextValue() - } -} - -private struct SizeModifier: ViewModifier { - func body(content: Content) -> some View { - content.background( - GeometryReader { geometry in - Color.clear - .preference(key: ImageSizePreferenceKey.self, value: geometry.size) - } - ) - } -} - @Observable private final class AsyncSwiftUIImageViewModel< Data: RenderDataType, diff --git a/AsyncImageViewTests/AsyncSwiftUIImageViewSpec.swift b/AsyncImageViewTests/AsyncSwiftUIImageViewSpec.swift new file mode 100644 index 0000000..647c83c --- /dev/null +++ b/AsyncImageViewTests/AsyncSwiftUIImageViewSpec.swift @@ -0,0 +1,42 @@ +import Quick +import Nimble +import SwiftUI +import UIKit + +import ReactiveSwift + +import AsyncImageView + +class AsyncSwiftUIImageViewSpec: QuickSpec { + override class func spec() { + describe("AsyncSwiftUIImageView") { + it("requests the full proposed size") { + let renderer = TestRenderer() + typealias ViewType = AsyncSwiftUIImageView< + TestRenderData, + TestData, + TestRenderer, + TestRenderer + > + let view = ViewType( + renderer: renderer, + placeholderRenderer: nil, + uiScheduler: ImmediateScheduler(), + imageCreationScheduler: ImmediateScheduler() + ) + .data(.a) + .frame(width: 300, height: 100) + let viewController = UIHostingController(rootView: view) + let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 300, height: 100))) + window.rootViewController = viewController + window.makeKeyAndVisible() + viewController.view.frame = window.bounds + viewController.view.layoutIfNeeded() + + expect(renderer.renderedImages.value).toEventually( + contain(TestRenderData(data: .a, size: window.bounds.size)) + ) + } + } + } +} From a9edc13c5a4481c706fa85c82bcd6379601b0159 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Sat, 11 Jul 2026 19:51:34 -0700 Subject: [PATCH 2/2] Address SwiftUI sizing review --- AsyncImageView/AsyncSwiftUIImageView.swift | 2 -- .../AsyncSwiftUIImageViewSpec.swift | 29 +++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index 9d68e3e..6d47202 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -58,8 +58,6 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { public var body: some View { ZStack { self.imageView - - Color.clear } .onGeometryChange(for: CGSize.self) { geometry in geometry.size diff --git a/AsyncImageViewTests/AsyncSwiftUIImageViewSpec.swift b/AsyncImageViewTests/AsyncSwiftUIImageViewSpec.swift index 647c83c..bfbe47c 100644 --- a/AsyncImageViewTests/AsyncSwiftUIImageViewSpec.swift +++ b/AsyncImageViewTests/AsyncSwiftUIImageViewSpec.swift @@ -11,12 +11,12 @@ class AsyncSwiftUIImageViewSpec: QuickSpec { override class func spec() { describe("AsyncSwiftUIImageView") { it("requests the full proposed size") { - let renderer = TestRenderer() + let renderer = SquareImageRenderer() typealias ViewType = AsyncSwiftUIImageView< TestRenderData, TestData, - TestRenderer, - TestRenderer + SquareImageRenderer, + SquareImageRenderer > let view = ViewType( renderer: renderer, @@ -33,10 +33,27 @@ class AsyncSwiftUIImageViewSpec: QuickSpec { viewController.view.frame = window.bounds viewController.view.layoutIfNeeded() - expect(renderer.renderedImages.value).toEventually( - contain(TestRenderData(data: .a, size: window.bounds.size)) - ) + let expectedRequest = TestRenderData(data: .a, size: window.bounds.size) + expect(renderer.renderedImages.value).toEventually(equal([expectedRequest])) + + RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) + viewController.view.layoutIfNeeded() + + expect(renderer.renderedImages.value) == [expectedRequest] } } } } + +private final class SquareImageRenderer: RendererType { + let renderedImages = Atomic<[TestRenderData]>([]) + + func renderImageWithData(_ data: TestRenderData) -> SignalProducer { + TestRenderer.rendererForSize(CGSize(width: 100, height: 100), scale: 1) + .asyncRenderer(ImmediateScheduler()) + .renderImageWithData(data) + .on(started: { + self.renderedImages.modify { $0.append(data) } + }) + } +}