From a566e84299a39fd02bb7f4ed0bca6838eacdb95b Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 2 Dec 2025 08:20:05 +0900 Subject: [PATCH 1/7] Refactor image request management --- AsyncImageView/AsyncSwiftUIImageView.swift | 78 +++++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index 0261dab..c1ea191 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -22,13 +22,11 @@ public struct AsyncSwiftUIImageView< PlaceholderRenderer.Data == Data, PlaceholderRenderer.Error == Never, Renderer.RenderResult == PlaceholderRenderer.RenderResult { - private typealias ImageLoader = AsyncImageLoader + private typealias ViewModel = AsyncSwiftUIImageViewModel private let renderer: Renderer private let placeholderRenderer: PlaceholderRenderer? private let uiScheduler: ReactiveSwift.Scheduler - private let requestsSignal: Signal - private let requestsObserver: Signal.Observer private let imageCreationScheduler: ReactiveSwift.Scheduler @@ -41,12 +39,9 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { self.placeholderRenderer = placeholderRenderer self.uiScheduler = uiScheduler self.imageCreationScheduler = imageCreationScheduler - - (self.requestsSignal, self.requestsObserver) = Signal.pipe() } - @State private var renderResult: Renderer.RenderResult? - @State private var disposable: Disposable? + @StateObject private var viewModel = ViewModel() public var data: ImageViewData? { didSet { @@ -74,28 +69,22 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { } } .onAppear { - self.disposable?.dispose() - self.disposable = ImageLoader.createSignal( - requestsSignal: self.requestsSignal, + self.viewModel.start( renderer: self.renderer, placeholderRenderer: self.placeholderRenderer, uiScheduler: self.uiScheduler, imageCreationScheduler: self.imageCreationScheduler ) - .observeValues { - self.renderResult = $0 - } - self.requestImage() } .onDisappear { - self.disposable?.dispose() + self.viewModel.stop() } } @ViewBuilder private var imageView: some View { - if let result = self.renderResult { + if let result = self.viewModel.renderResult { Image(uiImage: result.image) .resizable() .scaledToFit() @@ -116,8 +105,8 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { return } - self.imageCreationScheduler.schedule { [data, size, weak observer = self.requestsObserver] in - observer?.send(value: data?.renderDataWithSize(size)) + self.imageCreationScheduler.schedule { [data, size, viewModel = self.viewModel] in + viewModel.requestImage(data?.renderDataWithSize(size)) } } } @@ -151,3 +140,56 @@ private struct SizeModifier: ViewModifier { ) } } + +private final class AsyncSwiftUIImageViewModel< + Data: RenderDataType, + ImageViewData: ImageViewDataType, + Renderer: RendererType, + PlaceholderRenderer: RendererType +>: ObservableObject + where + ImageViewData.RenderData == Data, + Renderer.Data == Data, + Renderer.Error == Never, + PlaceholderRenderer.Data == Data, + PlaceholderRenderer.Error == Never, + Renderer.RenderResult == PlaceholderRenderer.RenderResult { + private typealias ImageLoader = AsyncImageLoader + + @Published private(set) var renderResult: Renderer.RenderResult? + + private let requestsSignal: Signal + private let requestsObserver: Signal.Observer + private var disposable: Disposable? + + init() { + (self.requestsSignal, self.requestsObserver) = Signal.pipe() + } + + func start( + renderer: Renderer, + placeholderRenderer: PlaceholderRenderer?, + uiScheduler: ReactiveSwift.Scheduler, + imageCreationScheduler: ReactiveSwift.Scheduler) { + self.disposable?.dispose() + self.disposable = ImageLoader.createSignal( + requestsSignal: self.requestsSignal, + renderer: renderer, + placeholderRenderer: placeholderRenderer, + uiScheduler: uiScheduler, + imageCreationScheduler: imageCreationScheduler + ) + .observeValues { [weak self] result in + self?.renderResult = result + } + } + + func stop() { + self.disposable?.dispose() + self.disposable = nil + } + + func requestImage(_ data: Data?) { + self.requestsObserver.send(value: data) + } +} From a76a4eb738eb3393c56c7ba6fdb45ff406813eac Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 2 Dec 2025 08:26:33 +0900 Subject: [PATCH 2/7] Embed dependencies in view model --- AsyncImageView/AsyncSwiftUIImageView.swift | 65 +++++++++++----------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index c1ea191..e290552 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -24,25 +24,23 @@ public struct AsyncSwiftUIImageView< Renderer.RenderResult == PlaceholderRenderer.RenderResult { private typealias ViewModel = AsyncSwiftUIImageViewModel - private let renderer: Renderer - private let placeholderRenderer: PlaceholderRenderer? - private let uiScheduler: ReactiveSwift.Scheduler - - private let imageCreationScheduler: ReactiveSwift.Scheduler + @StateObject private var viewModel: ViewModel public init( renderer: Renderer, placeholderRenderer: PlaceholderRenderer? = nil, uiScheduler: ReactiveSwift.Scheduler = UIScheduler(), imageCreationScheduler: ReactiveSwift.Scheduler = QueueScheduler()) { - self.renderer = renderer - self.placeholderRenderer = placeholderRenderer - self.uiScheduler = uiScheduler - self.imageCreationScheduler = imageCreationScheduler + _viewModel = StateObject( + wrappedValue: ViewModel( + renderer: renderer, + placeholderRenderer: placeholderRenderer, + uiScheduler: uiScheduler, + imageCreationScheduler: imageCreationScheduler + ) + ) } - @StateObject private var viewModel = ViewModel() - public var data: ImageViewData? { didSet { self.requestImage() @@ -69,12 +67,7 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { } } .onAppear { - self.viewModel.start( - renderer: self.renderer, - placeholderRenderer: self.placeholderRenderer, - uiScheduler: self.uiScheduler, - imageCreationScheduler: self.imageCreationScheduler - ) + self.viewModel.start() self.requestImage() } .onDisappear { @@ -105,9 +98,7 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { return } - self.imageCreationScheduler.schedule { [data, size, viewModel = self.viewModel] in - viewModel.requestImage(data?.renderDataWithSize(size)) - } + self.viewModel.requestImage(self.data, size: self.size) } } @@ -158,26 +149,36 @@ private final class AsyncSwiftUIImageViewModel< @Published private(set) var renderResult: Renderer.RenderResult? + private let renderer: Renderer + private let placeholderRenderer: PlaceholderRenderer? + private let uiScheduler: ReactiveSwift.Scheduler + private let imageCreationScheduler: ReactiveSwift.Scheduler + private let requestsSignal: Signal private let requestsObserver: Signal.Observer private var disposable: Disposable? - init() { - (self.requestsSignal, self.requestsObserver) = Signal.pipe() - } - - func start( + init( renderer: Renderer, placeholderRenderer: PlaceholderRenderer?, uiScheduler: ReactiveSwift.Scheduler, imageCreationScheduler: ReactiveSwift.Scheduler) { + self.renderer = renderer + self.placeholderRenderer = placeholderRenderer + self.uiScheduler = uiScheduler + self.imageCreationScheduler = imageCreationScheduler + + (self.requestsSignal, self.requestsObserver) = Signal.pipe() + } + + func start() { self.disposable?.dispose() self.disposable = ImageLoader.createSignal( requestsSignal: self.requestsSignal, - renderer: renderer, - placeholderRenderer: placeholderRenderer, - uiScheduler: uiScheduler, - imageCreationScheduler: imageCreationScheduler + renderer: self.renderer, + placeholderRenderer: self.placeholderRenderer, + uiScheduler: self.uiScheduler, + imageCreationScheduler: self.imageCreationScheduler ) .observeValues { [weak self] result in self?.renderResult = result @@ -189,7 +190,9 @@ private final class AsyncSwiftUIImageViewModel< self.disposable = nil } - func requestImage(_ data: Data?) { - self.requestsObserver.send(value: data) + func requestImage(_ data: ImageViewData?, size: CGSize) { + self.imageCreationScheduler.schedule { [data, size, observer = self.requestsObserver] in + observer.send(value: data?.renderDataWithSize(size)) + } } } From 15f85cd5f94a83be0f0f4ea5f69210d5cd8b6e02 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 2 Dec 2025 08:41:51 +0900 Subject: [PATCH 3/7] Raise platform deployment targets --- Example/Example.xcodeproj/project.pbxproj | 4 ++-- Package.swift | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 15f4356..e1a057b 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -258,7 +258,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -313,7 +313,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; diff --git a/Package.swift b/Package.swift index f8de81b..83add19 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,12 @@ import PackageDescription let package = Package( name: "AsyncImageView", - platforms: [.iOS(.v13), .tvOS(.v13), .watchOS(.v9)], + platforms: [ + .iOS(.init(18, 0, 0)), + .macOS(.init(15, 0, 0)), + .tvOS(.init(18, 0, 0)), + .watchOS(.init(11, 0, 0)) + ], products: [ .library(name: "AsyncImageView", targets: ["AsyncImageView"]) ], From d751ca4f737c868031298d48f413a61b9cf29446 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 2 Dec 2025 08:42:01 +0900 Subject: [PATCH 4/7] Adopt Observation view model --- AsyncImageView/AsyncSwiftUIImageView.swift | 20 ++++++++++++++------ Package.swift | 10 +++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index e290552..b53084c 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import Combine +import Observation import ReactiveSwift public struct AsyncSwiftUIImageView< @@ -24,15 +24,15 @@ public struct AsyncSwiftUIImageView< Renderer.RenderResult == PlaceholderRenderer.RenderResult { private typealias ViewModel = AsyncSwiftUIImageViewModel - @StateObject private var viewModel: ViewModel + @State private var viewModel: ViewModel public init( renderer: Renderer, placeholderRenderer: PlaceholderRenderer? = nil, uiScheduler: ReactiveSwift.Scheduler = UIScheduler(), imageCreationScheduler: ReactiveSwift.Scheduler = QueueScheduler()) { - _viewModel = StateObject( - wrappedValue: ViewModel( + _viewModel = State( + initialValue: ViewModel( renderer: renderer, placeholderRenderer: placeholderRenderer, uiScheduler: uiScheduler, @@ -132,12 +132,13 @@ private struct SizeModifier: ViewModifier { } } +@Observable private final class AsyncSwiftUIImageViewModel< Data: RenderDataType, ImageViewData: ImageViewDataType, Renderer: RendererType, PlaceholderRenderer: RendererType ->: ObservableObject +> where ImageViewData.RenderData == Data, Renderer.Data == Data, @@ -147,15 +148,22 @@ private final class AsyncSwiftUIImageViewModel< Renderer.RenderResult == PlaceholderRenderer.RenderResult { private typealias ImageLoader = AsyncImageLoader - @Published private(set) var renderResult: Renderer.RenderResult? + var renderResult: Renderer.RenderResult? + @ObservationIgnored private let renderer: Renderer + @ObservationIgnored private let placeholderRenderer: PlaceholderRenderer? + @ObservationIgnored private let uiScheduler: ReactiveSwift.Scheduler + @ObservationIgnored private let imageCreationScheduler: ReactiveSwift.Scheduler + @ObservationIgnored private let requestsSignal: Signal + @ObservationIgnored private let requestsObserver: Signal.Observer + @ObservationIgnored private var disposable: Disposable? init( diff --git a/Package.swift b/Package.swift index 83add19..5d62385 100644 --- a/Package.swift +++ b/Package.swift @@ -1,13 +1,13 @@ -// swift-tools-version:5.7 +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "AsyncImageView", platforms: [ - .iOS(.init(18, 0, 0)), - .macOS(.init(15, 0, 0)), - .tvOS(.init(18, 0, 0)), - .watchOS(.init(11, 0, 0)) + .iOS(.v18), + .macOS(.v15), + .tvOS(.v18), + .watchOS(.v11) ], products: [ .library(name: "AsyncImageView", targets: ["AsyncImageView"]) From 19b69fdd95aea4ed044018454c155989cc8abcd2 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 2 Dec 2025 08:47:27 +0900 Subject: [PATCH 5/7] Tidy observation annotations --- AsyncImageView/AsyncSwiftUIImageView.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index b53084c..228488a 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -150,18 +150,12 @@ private final class AsyncSwiftUIImageViewModel< var renderResult: Renderer.RenderResult? - @ObservationIgnored private let renderer: Renderer - @ObservationIgnored private let placeholderRenderer: PlaceholderRenderer? - @ObservationIgnored private let uiScheduler: ReactiveSwift.Scheduler - @ObservationIgnored private let imageCreationScheduler: ReactiveSwift.Scheduler - @ObservationIgnored private let requestsSignal: Signal - @ObservationIgnored private let requestsObserver: Signal.Observer @ObservationIgnored private var disposable: Disposable? From 0ab27a2c3d8a0be4e1b40794038b839db11c1b3a Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 2 Dec 2025 08:47:53 +0900 Subject: [PATCH 6/7] Update AsyncSwiftUIImageView.swift --- AsyncImageView/AsyncSwiftUIImageView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index 228488a..4b460a7 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -7,7 +7,6 @@ // import SwiftUI -import Observation import ReactiveSwift public struct AsyncSwiftUIImageView< From fd37cc14fbe09d14c6b762f7b374abce8dc125e3 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 2 Dec 2025 08:48:58 +0900 Subject: [PATCH 7/7] Update AsyncSwiftUIImageView.swift --- AsyncImageView/AsyncSwiftUIImageView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index 4b460a7..4f17e2b 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -147,7 +147,7 @@ private final class AsyncSwiftUIImageViewModel< Renderer.RenderResult == PlaceholderRenderer.RenderResult { private typealias ImageLoader = AsyncImageLoader - var renderResult: Renderer.RenderResult? + private(set) var renderResult: Renderer.RenderResult? private let renderer: Renderer private let placeholderRenderer: PlaceholderRenderer?