diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index ee0c1d4..108f26e 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.6' + 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/AsyncImageView/AsyncImageView.swift b/AsyncImageView/AsyncImageView.swift index 623b962..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 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..62f057c 100644 --- a/AsyncImageView/Renderers/ViewRenderer.swift +++ b/AsyncImageView/Renderers/ViewRenderer.swift @@ -9,14 +9,15 @@ import UIKit import CoreGraphics -import ReactiveSwift +@preconcurrency import ReactiveSwift #if !os(watchOS) /// `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,27 +84,32 @@ 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() + 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.hasEnded { + observer.send(value: renderBlock(view)) + observer.sendCompleted() + } + } } } } .start(on: UIScheduler()) } +@MainActor fileprivate func draw(view: UIView, inContext context: CGContext) { view.layer.render(in: context) } 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() } diff --git a/Package.swift b/Package.swift index 673fc4a..f5366eb 100644 --- a/Package.swift +++ b/Package.swift @@ -19,15 +19,23 @@ let package = Package( .target( name: "AsyncImageView", dependencies: ["ReactiveSwift"], - path: "AsyncImageView" + path: "AsyncImageView", + swiftSettings: [ + // Work around https://github.com/swiftlang/swift/issues/75453. + .unsafeFlags(["-disable-dynamic-actor-isolation"]) + ] ), .testTarget( name: "AsyncImageViewTests", dependencies: [ "AsyncImageView" ], - path: "AsyncImageViewTests" + path: "AsyncImageViewTests", + swiftSettings: [ + // Work around https://github.com/swiftlang/swift/issues/75453. + .unsafeFlags(["-disable-dynamic-actor-isolation"]) + ] ) ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v6] )