From bf2d144183df7b5f36ce874134c83c118c8bf72a Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Fri, 24 Jul 2026 10:22:34 -0700 Subject: [PATCH 1/4] Validate Swift 6 library compilation --- .github/workflows/build-and-test.yml | 21 +++++++++++++++++++++ Package.swift | 12 +++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index ee0c1d4..4ef433c 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -8,6 +8,27 @@ concurrency: cancel-in-progress: true jobs: + swift-6-build: + runs-on: macos-26 + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Select Xcode + uses: mxcl/xcodebuild@v3 + with: + xcode: '26.5' + action: none + + - name: Build library in Swift 6 mode + run: | + swift build \ + --target AsyncImageView \ + --triple arm64-apple-ios18.0-simulator \ + --sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" + test: runs-on: macos-26 timeout-minutes: 30 diff --git a/Package.swift b/Package.swift index 673fc4a..9e43393 100644 --- a/Package.swift +++ b/Package.swift @@ -19,15 +19,21 @@ let package = Package( .target( name: "AsyncImageView", dependencies: ["ReactiveSwift"], - path: "AsyncImageView" + path: "AsyncImageView", + swiftSettings: [ + .unsafeFlags(["-disable-dynamic-actor-isolation"]) + ] ), .testTarget( name: "AsyncImageViewTests", dependencies: [ "AsyncImageView" ], - path: "AsyncImageViewTests" + path: "AsyncImageViewTests", + swiftSettings: [ + .unsafeFlags(["-disable-dynamic-actor-isolation"]) + ] ) ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v6] ) From 82dfc969bb418bc114b237f42cb6e776b846a2a3 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Fri, 24 Jul 2026 10:24:45 -0700 Subject: [PATCH 2/4] Run Swift 6 CI on Xcode 26.6 --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 4ef433c..108f26e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,7 +19,7 @@ jobs: - name: Select Xcode uses: mxcl/xcodebuild@v3 with: - xcode: '26.5' + xcode: '26.6' action: none - name: Build library in Swift 6 mode From b7769e41d27ddbb0b1970ec76db6fd00fae05987 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Fri, 24 Jul 2026 10:27:18 -0700 Subject: [PATCH 3/4] Fix Swift 6 concurrency errors --- AsyncImageView/AsyncImageView.swift | 2 +- .../Renderers/ImageInflaterRenderer.swift | 2 +- AsyncImageView/Renderers/Renderer.swift | 2 +- AsyncImageView/Renderers/ViewRenderer.swift | 48 ++++++++++++------- AsyncImageView/SynchronousUIScheduler.swift | 2 +- 5 files changed, 35 insertions(+), 21 deletions(-) diff --git a/AsyncImageView/AsyncImageView.swift b/AsyncImageView/AsyncImageView.swift index 623b962..d6d9e10 100644 --- a/AsyncImageView/AsyncImageView.swift +++ b/AsyncImageView/AsyncImageView.swift @@ -39,7 +39,7 @@ open class AsyncImageView< private let imageCreationScheduler: ReactiveSwift.Scheduler - private var disposable: Disposable? + nonisolated(unsafe) private var disposable: Disposable? public init( initialFrame: CGRect, diff --git a/AsyncImageView/Renderers/ImageInflaterRenderer.swift b/AsyncImageView/Renderers/ImageInflaterRenderer.swift index 626385a..36d1268 100644 --- a/AsyncImageView/Renderers/ImageInflaterRenderer.swift +++ b/AsyncImageView/Renderers/ImageInflaterRenderer.swift @@ -73,7 +73,7 @@ public final class ImageInflaterRenderer: RendererType { } } -public enum ImageInflaterRendererContentMode { +public enum ImageInflaterRendererContentMode: Sendable { case aspectFill case aspectFit diff --git a/AsyncImageView/Renderers/Renderer.swift b/AsyncImageView/Renderers/Renderer.swift index d45addf..b067f1b 100644 --- a/AsyncImageView/Renderers/Renderer.swift +++ b/AsyncImageView/Renderers/Renderer.swift @@ -11,7 +11,7 @@ import UIKit import ReactiveSwift /// Information required to produce an image -public protocol RenderDataType: Hashable { +public protocol RenderDataType: Hashable, Sendable { var size: CGSize { get } } diff --git a/AsyncImageView/Renderers/ViewRenderer.swift b/AsyncImageView/Renderers/ViewRenderer.swift index 48a9e16..99922a7 100644 --- a/AsyncImageView/Renderers/ViewRenderer.swift +++ b/AsyncImageView/Renderers/ViewRenderer.swift @@ -15,8 +15,9 @@ import ReactiveSwift /// `RendererType` which generates a `UIImage` from a UIView. @available(iOS 10.0, tvOSApplicationExtension 10.0, *) -public final class ViewRenderer: RendererType { - public typealias Block = (_ data: Data) -> UIView +@MainActor +public final class ViewRenderer: @MainActor RendererType { + public typealias Block = @MainActor (_ data: Data) -> UIView private let format: UIGraphicsImageRendererFormat private let viewCreationBlock: Block @@ -49,8 +50,9 @@ public final class ViewRenderer: RendererType { } /// `RendererType` which generates a `UIImage` from a UIView. -public final class OldViewRenderer: RendererType { - public typealias Block = (_ data: Data) -> UIView +@MainActor +public final class OldViewRenderer: @MainActor RendererType { + public typealias Block = @MainActor (_ data: Data) -> UIView private let opaque: Bool private let viewCreationBlock: Block @@ -82,29 +84,41 @@ public final class OldViewRenderer: RendererType { fileprivate func createProducer( _ data: Data, - viewCreationBlock: @escaping (_ data: Data) -> UIView, - renderBlock: @escaping (UIView) -> UIImage + viewCreationBlock: @escaping @MainActor (_ data: Data) -> UIView, + renderBlock: @escaping @MainActor (UIView) -> UIImage ) -> SignalProducer { return SignalProducer { observer, lifetime in - let view = viewCreationBlock(data) - view.frame.origin = .zero - view.bounds.size = data.size - view.layoutIfNeeded() - - // Make the CA renderer wait "until all the post-commit triggers fire". - // We can't take a snapshot right away because the view has not been commited to the render server yet. - DispatchQueue.main.async { - if !lifetime.hasEnded { - observer.send(value: renderBlock(view)) - observer.sendCompleted() + let observer = UncheckedSendableBox(value: observer) + let lifetime = UncheckedSendableBox(value: lifetime) + + MainActor.assumeIsolated { + let view = viewCreationBlock(data) + view.frame.origin = .zero + view.bounds.size = data.size + view.layoutIfNeeded() + + // Make the CA renderer wait "until all the post-commit triggers fire". + // We can't take a snapshot right away because the view has not been commited to the render server yet. + UIScheduler().schedule { + MainActor.assumeIsolated { + if !lifetime.value.hasEnded { + observer.value.send(value: renderBlock(view)) + observer.value.sendCompleted() + } + } } } } .start(on: UIScheduler()) } +@MainActor fileprivate func draw(view: UIView, inContext context: CGContext) { view.layer.render(in: context) } +fileprivate struct UncheckedSendableBox: @unchecked Sendable { + let value: Value +} + #endif diff --git a/AsyncImageView/SynchronousUIScheduler.swift b/AsyncImageView/SynchronousUIScheduler.swift index dcecbe2..bd050ed 100644 --- a/AsyncImageView/SynchronousUIScheduler.swift +++ b/AsyncImageView/SynchronousUIScheduler.swift @@ -22,7 +22,7 @@ internal final class SynchronousUIScheduler: Scheduler { if Thread.isMainThread { action() } else { - DispatchQueue.main.async { + UIScheduler().schedule { if !disposable.isDisposed { action() } From e0636d54253d4e9c9bfa0c911410e69f2caa865c Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Fri, 24 Jul 2026 10:32:28 -0700 Subject: [PATCH 4/4] Prefer preconcurrency ReactiveSwift imports --- AsyncImageView/AsyncImageView.swift | 4 ++-- AsyncImageView/Renderers/ViewRenderer.swift | 15 ++++----------- Package.swift | 2 ++ 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/AsyncImageView/AsyncImageView.swift b/AsyncImageView/AsyncImageView.swift index d6d9e10..0546fc9 100644 --- a/AsyncImageView/AsyncImageView.swift +++ b/AsyncImageView/AsyncImageView.swift @@ -9,7 +9,7 @@ import UIKit import Combine -import ReactiveSwift +@preconcurrency import ReactiveSwift public protocol ImageViewDataType { associatedtype RenderData: RenderDataType @@ -39,7 +39,7 @@ open class AsyncImageView< private let imageCreationScheduler: ReactiveSwift.Scheduler - nonisolated(unsafe) private var disposable: Disposable? + private var disposable: Disposable? public init( initialFrame: CGRect, diff --git a/AsyncImageView/Renderers/ViewRenderer.swift b/AsyncImageView/Renderers/ViewRenderer.swift index 99922a7..62f057c 100644 --- a/AsyncImageView/Renderers/ViewRenderer.swift +++ b/AsyncImageView/Renderers/ViewRenderer.swift @@ -9,7 +9,7 @@ import UIKit import CoreGraphics -import ReactiveSwift +@preconcurrency import ReactiveSwift #if !os(watchOS) @@ -88,9 +88,6 @@ fileprivate func createProducer( renderBlock: @escaping @MainActor (UIView) -> UIImage ) -> SignalProducer { return SignalProducer { observer, lifetime in - let observer = UncheckedSendableBox(value: observer) - let lifetime = UncheckedSendableBox(value: lifetime) - MainActor.assumeIsolated { let view = viewCreationBlock(data) view.frame.origin = .zero @@ -101,9 +98,9 @@ fileprivate func createProducer( // We can't take a snapshot right away because the view has not been commited to the render server yet. UIScheduler().schedule { MainActor.assumeIsolated { - if !lifetime.value.hasEnded { - observer.value.send(value: renderBlock(view)) - observer.value.sendCompleted() + if !lifetime.hasEnded { + observer.send(value: renderBlock(view)) + observer.sendCompleted() } } } @@ -117,8 +114,4 @@ fileprivate func draw(view: UIView, inContext context: CGContext) { view.layer.render(in: context) } -fileprivate struct UncheckedSendableBox: @unchecked Sendable { - let value: Value -} - #endif diff --git a/Package.swift b/Package.swift index 9e43393..f5366eb 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,7 @@ let package = Package( dependencies: ["ReactiveSwift"], path: "AsyncImageView", swiftSettings: [ + // Work around https://github.com/swiftlang/swift/issues/75453. .unsafeFlags(["-disable-dynamic-actor-isolation"]) ] ), @@ -31,6 +32,7 @@ let package = Package( ], path: "AsyncImageViewTests", swiftSettings: [ + // Work around https://github.com/swiftlang/swift/issues/75453. .unsafeFlags(["-disable-dynamic-actor-isolation"]) ] )