From 6c523f8e09d2f77b657c656877fde81791dba237 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Thu, 3 Sep 2026 11:22:18 -0700 Subject: [PATCH 1/2] Support SwiftUI image sizing and replacements --- AsyncImageView/AsyncSwiftUIImageView.swift | 45 ++++++++++++++++--- .../AsyncSwiftUIImageViewStateTests.swift | 2 + 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index 4f4efd5..4b2c2d7 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -9,6 +9,12 @@ import SwiftUI import ReactiveSwift +public enum AsyncSwiftUIImageContentMode: Sendable { + case scaleToFill + case scaleAspectFit + case scaleAspectFill +} + public struct AsyncSwiftUIImageView< Data: RenderDataType, ImageViewData: ImageViewDataType, @@ -25,17 +31,21 @@ public struct AsyncSwiftUIImageView< @State private var viewModelReference: LazyReference + private let contentMode: AsyncSwiftUIImageContentMode + private var viewModel: ViewModel { self.viewModelReference.value } public init( renderer: Renderer, - placeholderRenderer: PlaceholderRenderer? = nil + placeholderRenderer: PlaceholderRenderer? = nil, + contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit ) { self.init( renderer: renderer, placeholderRenderer: placeholderRenderer, + contentMode: contentMode, uiSchedulerFactory: { UIScheduler() }, imageCreationSchedulerFactory: { QueueScheduler() } ) @@ -44,11 +54,13 @@ public struct AsyncSwiftUIImageView< public init( renderer: Renderer, placeholderRenderer: PlaceholderRenderer? = nil, - uiScheduler: ReactiveSwift.Scheduler + uiScheduler: ReactiveSwift.Scheduler, + contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit ) { self.init( renderer: renderer, placeholderRenderer: placeholderRenderer, + contentMode: contentMode, uiSchedulerFactory: { uiScheduler }, imageCreationSchedulerFactory: { QueueScheduler() } ) @@ -57,11 +69,13 @@ public struct AsyncSwiftUIImageView< public init( renderer: Renderer, placeholderRenderer: PlaceholderRenderer? = nil, - imageCreationScheduler: ReactiveSwift.Scheduler + imageCreationScheduler: ReactiveSwift.Scheduler, + contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit ) { self.init( renderer: renderer, placeholderRenderer: placeholderRenderer, + contentMode: contentMode, uiSchedulerFactory: { UIScheduler() }, imageCreationSchedulerFactory: { imageCreationScheduler } ) @@ -71,11 +85,13 @@ public struct AsyncSwiftUIImageView< renderer: Renderer, placeholderRenderer: PlaceholderRenderer? = nil, uiScheduler: ReactiveSwift.Scheduler, - imageCreationScheduler: ReactiveSwift.Scheduler + imageCreationScheduler: ReactiveSwift.Scheduler, + contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit ) { self.init( renderer: renderer, placeholderRenderer: placeholderRenderer, + contentMode: contentMode, uiSchedulerFactory: { uiScheduler }, imageCreationSchedulerFactory: { imageCreationScheduler } ) @@ -84,9 +100,11 @@ public struct AsyncSwiftUIImageView< internal init( renderer: Renderer, placeholderRenderer: PlaceholderRenderer?, + contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit, uiSchedulerFactory: @escaping () -> ReactiveSwift.Scheduler, imageCreationSchedulerFactory: @escaping () -> ReactiveSwift.Scheduler ) { + self.contentMode = contentMode _viewModelReference = State( initialValue: LazyReference { ViewModel( @@ -135,9 +153,8 @@ public struct AsyncSwiftUIImageView< @ViewBuilder private var imageView: some View { if let result = self.viewModel.renderResult { - Image(uiImage: result.image) - .resizable() - .scaledToFit() + self.image(result.image) + .id(ObjectIdentifier(result.image)) .transition( AnyTransition.opacity.animation( result.cacheHit @@ -150,6 +167,20 @@ public struct AsyncSwiftUIImageView< } } + @ViewBuilder + private func image(_ image: UIImage) -> some View { + let image = Image(uiImage: image).resizable() + + switch self.contentMode { + case .scaleToFill: + image + case .scaleAspectFit: + image.scaledToFit() + case .scaleAspectFill: + image.scaledToFill() + } + } + private func requestImage() { guard self.size.width > 0 && self.size.height > 0 else { return diff --git a/AsyncImageViewTests/AsyncSwiftUIImageViewStateTests.swift b/AsyncImageViewTests/AsyncSwiftUIImageViewStateTests.swift index 4790f23..d53839c 100644 --- a/AsyncImageViewTests/AsyncSwiftUIImageViewStateTests.swift +++ b/AsyncImageViewTests/AsyncSwiftUIImageViewStateTests.swift @@ -13,6 +13,8 @@ struct AsyncSwiftUIImageViewStateTests { let renderer = StateTestRenderer() _ = StateTestImageView(renderer: renderer) + _ = StateTestImageView(renderer: renderer, contentMode: .scaleToFill) + _ = StateTestImageView(renderer: renderer, contentMode: .scaleAspectFill) _ = StateTestImageView(renderer: renderer, uiScheduler: ImmediateScheduler()) _ = StateTestImageView(renderer: renderer, imageCreationScheduler: ImmediateScheduler()) _ = StateTestImageView( From 6e01b2ec20c14b6488d2d006037bb69b154432f1 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Thu, 3 Sep 2026 11:48:56 -0700 Subject: [PATCH 2/2] Use SwiftUI content mode directly --- AsyncImageView/AsyncSwiftUIImageView.swift | 36 +++++-------------- .../AsyncSwiftUIImageViewStateTests.swift | 3 +- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index 4b2c2d7..52b83ee 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -9,12 +9,6 @@ import SwiftUI import ReactiveSwift -public enum AsyncSwiftUIImageContentMode: Sendable { - case scaleToFill - case scaleAspectFit - case scaleAspectFill -} - public struct AsyncSwiftUIImageView< Data: RenderDataType, ImageViewData: ImageViewDataType, @@ -31,7 +25,7 @@ public struct AsyncSwiftUIImageView< @State private var viewModelReference: LazyReference - private let contentMode: AsyncSwiftUIImageContentMode + private let contentMode: ContentMode private var viewModel: ViewModel { self.viewModelReference.value @@ -40,7 +34,7 @@ public struct AsyncSwiftUIImageView< public init( renderer: Renderer, placeholderRenderer: PlaceholderRenderer? = nil, - contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit + contentMode: ContentMode = .fit ) { self.init( renderer: renderer, @@ -55,7 +49,7 @@ public struct AsyncSwiftUIImageView< renderer: Renderer, placeholderRenderer: PlaceholderRenderer? = nil, uiScheduler: ReactiveSwift.Scheduler, - contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit + contentMode: ContentMode = .fit ) { self.init( renderer: renderer, @@ -70,7 +64,7 @@ public struct AsyncSwiftUIImageView< renderer: Renderer, placeholderRenderer: PlaceholderRenderer? = nil, imageCreationScheduler: ReactiveSwift.Scheduler, - contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit + contentMode: ContentMode = .fit ) { self.init( renderer: renderer, @@ -86,7 +80,7 @@ public struct AsyncSwiftUIImageView< placeholderRenderer: PlaceholderRenderer? = nil, uiScheduler: ReactiveSwift.Scheduler, imageCreationScheduler: ReactiveSwift.Scheduler, - contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit + contentMode: ContentMode = .fit ) { self.init( renderer: renderer, @@ -100,7 +94,7 @@ public struct AsyncSwiftUIImageView< internal init( renderer: Renderer, placeholderRenderer: PlaceholderRenderer?, - contentMode: AsyncSwiftUIImageContentMode = .scaleAspectFit, + contentMode: ContentMode = .fit, uiSchedulerFactory: @escaping () -> ReactiveSwift.Scheduler, imageCreationSchedulerFactory: @escaping () -> ReactiveSwift.Scheduler ) { @@ -153,7 +147,9 @@ public struct AsyncSwiftUIImageView< @ViewBuilder private var imageView: some View { if let result = self.viewModel.renderResult { - self.image(result.image) + Image(uiImage: result.image) + .resizable() + .aspectRatio(nil, contentMode: self.contentMode) .id(ObjectIdentifier(result.image)) .transition( AnyTransition.opacity.animation( @@ -167,20 +163,6 @@ public struct AsyncSwiftUIImageView< } } - @ViewBuilder - private func image(_ image: UIImage) -> some View { - let image = Image(uiImage: image).resizable() - - switch self.contentMode { - case .scaleToFill: - image - case .scaleAspectFit: - image.scaledToFit() - case .scaleAspectFill: - image.scaledToFill() - } - } - private func requestImage() { guard self.size.width > 0 && self.size.height > 0 else { return diff --git a/AsyncImageViewTests/AsyncSwiftUIImageViewStateTests.swift b/AsyncImageViewTests/AsyncSwiftUIImageViewStateTests.swift index d53839c..046ad7a 100644 --- a/AsyncImageViewTests/AsyncSwiftUIImageViewStateTests.swift +++ b/AsyncImageViewTests/AsyncSwiftUIImageViewStateTests.swift @@ -13,8 +13,7 @@ struct AsyncSwiftUIImageViewStateTests { let renderer = StateTestRenderer() _ = StateTestImageView(renderer: renderer) - _ = StateTestImageView(renderer: renderer, contentMode: .scaleToFill) - _ = StateTestImageView(renderer: renderer, contentMode: .scaleAspectFill) + _ = StateTestImageView(renderer: renderer, contentMode: .fill) _ = StateTestImageView(renderer: renderer, uiScheduler: ImmediateScheduler()) _ = StateTestImageView(renderer: renderer, imageCreationScheduler: ImmediateScheduler()) _ = StateTestImageView(