From eaeea198c5b48ef57ff59772344841c782affca4 Mon Sep 17 00:00:00 2001 From: Muukii Date: Fri, 21 Feb 2025 23:36:42 +0900 Subject: [PATCH] Patch --- Package.swift | 13 +- .../AsyncMultiplexImage.swift | 237 ++++++++++-------- .../AsyncMultiplexImage/DownloadManager.swift | 21 +- Sources/AsyncMultiplexImageDemo/Demo.swift | 108 -------- 4 files changed, 141 insertions(+), 238 deletions(-) delete mode 100644 Sources/AsyncMultiplexImageDemo/Demo.swift diff --git a/Package.swift b/Package.swift index 9c8aee4..f5bacd3 100644 --- a/Package.swift +++ b/Package.swift @@ -9,21 +9,14 @@ let package = Package( .iOS(.v16), ], products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "AsyncMultiplexImage", targets: ["AsyncMultiplexImage"] - ), - + ), .library( name: "AsyncMultiplexImage-Nuke", targets: ["AsyncMultiplexImage-Nuke"] ), - - .library( - name: "AsyncMultiplexImageDemo", - targets: ["AsyncMultiplexImageDemo"] - ) ], dependencies: [ .package(url: "https://github.com/kean/Nuke.git", from: "12.0.0"), @@ -38,10 +31,6 @@ let package = Package( name: "AsyncMultiplexImage-Nuke", dependencies: ["Nuke", "AsyncMultiplexImage"] ), - .target( - name: "AsyncMultiplexImageDemo", - dependencies: ["AsyncMultiplexImage", "AsyncMultiplexImage-Nuke", "Nuke"] - ), .testTarget( name: "AsyncMultiplexImageTests", dependencies: ["AsyncMultiplexImage"] diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift index 9311354..41e8942 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift @@ -4,7 +4,7 @@ import SwiftUISupportBackport import os.log enum Log { - + static func debug( file: StaticString = #file, line: UInt = #line, @@ -20,7 +20,7 @@ enum Log { "\(line.description)" ) } - + static func error( file: StaticString = #file, line: UInt = #line, @@ -36,42 +36,42 @@ enum Log { "\(line.description)" ) } - + } extension OSLog { - + @inline(__always) private static func makeOSLogInDebug(isEnabled: Bool = true, _ factory: () -> OSLog) -> OSLog { - #if DEBUG - if ProcessInfo.processInfo.environment["ASYNC_MULTIPLEX_IMAGE_LOG_ENABLED"] == "1" { - return factory() - } else { - return .disabled - } - #else +#if DEBUG + if ProcessInfo.processInfo.environment["ASYNC_MULTIPLEX_IMAGE_LOG_ENABLED"] == "1" { + return factory() + } else { return .disabled - #endif + } +#else + return .disabled +#endif } - + static let generic: OSLog = makeOSLogInDebug(isEnabled: false) { OSLog.init(subsystem: "app.muukii", category: "default") } static let view: OSLog = makeOSLogInDebug { OSLog.init(subsystem: "app.muukii", category: "SwiftUIVersion") } - + static let uiKit: OSLog = makeOSLogInDebug { OSLog.init(subsystem: "app.muukii", category: "UIKitVersion") } - + } public protocol AsyncMultiplexImageDownloader: Actor { - + func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws - -> UIImage - + -> UIImage + } public enum AsyncMultiplexImagePhase { @@ -82,10 +82,10 @@ public enum AsyncMultiplexImagePhase { } public struct AsyncMultiplexImageCandidate: Hashable, Sendable { - + public let index: Int public let urlRequest: URLRequest - + } public enum ImageRepresentation: Equatable { @@ -96,13 +96,13 @@ public enum ImageRepresentation: Equatable { public struct AsyncMultiplexImage< Content: AsyncMultiplexImageContent, Downloader: AsyncMultiplexImageDownloader >: View { - + private let imageRepresentation: ImageRepresentation private let downloader: Downloader private let content: Content - + private let clearsContentBeforeDownload: Bool - + // convenience init public init( multiplexImage: MultiplexImage, @@ -117,21 +117,21 @@ public struct AsyncMultiplexImage< content: content ) } - + public init( imageRepresentation: ImageRepresentation, downloader: Downloader, clearsContentBeforeDownload: Bool = true, content: Content ) { - + self.clearsContentBeforeDownload = clearsContentBeforeDownload self.imageRepresentation = imageRepresentation self.downloader = downloader self.content = content - + } - + public var body: some View { _AsyncMultiplexImage( clearsContentBeforeDownload: clearsContentBeforeDownload, @@ -140,47 +140,47 @@ public struct AsyncMultiplexImage< content: content ) } - -} +} + private struct _AsyncMultiplexImage< Content: AsyncMultiplexImageContent, Downloader: AsyncMultiplexImageDownloader >: View { - + private struct UpdateTrigger: Equatable { let size: CGSize let image: ImageRepresentation } - + @State private var item: ResultContainer.ItemSwiftUI? @State private var displaySize: CGSize = .zero @Environment(\.displayScale) var displayScale - + private let imageRepresentation: ImageRepresentation private let downloader: Downloader private let content: Content private let clearsContentBeforeDownload: Bool - + public init( clearsContentBeforeDownload: Bool, imageRepresentation: ImageRepresentation, downloader: Downloader, content: Content ) { - + self.clearsContentBeforeDownload = clearsContentBeforeDownload self.imageRepresentation = imageRepresentation self.downloader = downloader self.content = content } - + public var body: some View { - + Color.clear .overlay( content.body( phase: { - switch item { + switch item?.phase { case .none: return .empty case .some(.progress(let image)): @@ -199,96 +199,111 @@ private struct _AsyncMultiplexImage< displaySize = newValue } ) - .task(id: UpdateTrigger( - size: displaySize, - image: imageRepresentation - ), { - - await withTaskCancellationHandler { - - let newSize = displaySize + .task( + id: UpdateTrigger( + size: displaySize, + image: imageRepresentation + ), + { - guard newSize.height > 0 && newSize.width > 0 else { + if let item, + case .final = item.phase, + item.source == imageRepresentation { + // already final item loaded return } - if clearsContentBeforeDownload { - var transaction = Transaction() - transaction.disablesAnimations = true - withTransaction(transaction) { - self.item = nil - } - } - - switch imageRepresentation { - case .remote(let multiplexImage): - - let displayScale = self.displayScale - let candidates = await pushBackground { - - // making new candidates - let context = MultiplexImage.Context( - targetSize: newSize, - displayScale: displayScale - ) - - let urls = multiplexImage.makeURLs(context: context) - - let candidates = urls.enumerated().map { i, e in - AsyncMultiplexImageCandidate(index: i, urlRequest: .init(url: e)) - } - - return candidates - } + await withTaskCancellationHandler { + + let newSize = displaySize - guard Task.isCancelled == false else { + guard newSize.height > 0 && newSize.width > 0 else { return } - let stream = await DownloadManager.shared.start( - source: multiplexImage, - candidates: candidates, - downloader: downloader, - displaySize: newSize - ) - - guard Task.isCancelled == false else { - return + if clearsContentBeforeDownload { + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + self.item = nil + } } - do { - for try await item in stream { + switch imageRepresentation { + case .remote(let multiplexImage): + + let displayScale = self.displayScale + let candidates = await pushBackground { - guard Task.isCancelled == false else { - return - } + // making new candidates + let context = MultiplexImage.Context( + targetSize: newSize, + displayScale: displayScale + ) + + let urls = multiplexImage.makeURLs(context: context) - await MainActor.run { - self.item = item.swiftUI + let candidates = urls.enumerated().map { i, e in + AsyncMultiplexImageCandidate(index: i, urlRequest: .init(url: e)) } + + return candidates } - } catch { - // FIXME: Error handling - } - - case .loaded(let image): - - self.item = .final(image) - - } - } onCancel: { - // handle cancel - } - - }) + + guard Task.isCancelled == false else { + return + } + + let stream = await DownloadManager.shared.start( + source: multiplexImage, + candidates: candidates, + downloader: downloader, + displaySize: newSize + ) + + guard Task.isCancelled == false else { + return + } + + do { + for try await item in stream { + + guard Task.isCancelled == false else { + return + } + + await MainActor.run { + self.item = .init( + source: imageRepresentation, + phase: item.swiftUI + ) + } + } + } catch { + // FIXME: Error handling + } + + case .loaded(let image): + + self.item = .init( + source: imageRepresentation, + phase: .final(image) + ) + + } + } onCancel: { + // handle cancel + } + + }) .clipped(antialiased: true) -// .onDisappear { -// self.task?.cancel() -// self.task = nil -// } - + // .onDisappear { + // self.task?.cancel() + // self.task = nil + // } + } - + } private func pushBackground(task: @Sendable () -> sending Result) async -> sending Result { diff --git a/Sources/AsyncMultiplexImage/DownloadManager.swift b/Sources/AsyncMultiplexImage/DownloadManager.swift index 254dccf..f7ca73a 100644 --- a/Sources/AsyncMultiplexImage/DownloadManager.swift +++ b/Sources/AsyncMultiplexImage/DownloadManager.swift @@ -7,10 +7,10 @@ import SwiftUI actor DownloadManager { - + @MainActor static let shared = DownloadManager() - + private init() {} func start( @@ -19,7 +19,7 @@ actor DownloadManager { downloader: any AsyncMultiplexImageDownloader, displaySize: CGSize ) async -> sending AsyncThrowingStream { - + // this instance will be alive until finish let container = ResultContainer() @@ -41,7 +41,7 @@ actor ResultContainer { case progress(UIImage) case final(UIImage) - var swiftUI: ItemSwiftUI { + var swiftUI: ItemSwiftUI.Phase { switch self { case .progress(let image): return .progress(.init(uiImage: image).renderingMode(.original)) @@ -51,9 +51,16 @@ actor ResultContainer { } } - enum ItemSwiftUI { - case progress(Image) - case final(Image) + struct ItemSwiftUI: Equatable { + + enum Phase: Equatable { + case progress(Image) + case final(Image) + } + + let source: ImageRepresentation + let phase: Phase + } private var referenceCount: UInt64 = 0 diff --git a/Sources/AsyncMultiplexImageDemo/Demo.swift b/Sources/AsyncMultiplexImageDemo/Demo.swift deleted file mode 100644 index 046e6bf..0000000 --- a/Sources/AsyncMultiplexImageDemo/Demo.swift +++ /dev/null @@ -1,108 +0,0 @@ -import AsyncMultiplexImage -import AsyncMultiplexImage_Nuke -import Nuke -import SwiftUI - -func buildURLs(baseURLString: String, size: CGSize) -> [URL] { - - var components = URLComponents(string: baseURLString)! - - return [ - "", - "w=100", - "w=50", - "w=10", - ].map { - - components.query = $0 - - return components.url! - - } - -} - -struct AsyncMultiplexImage_Previews: PreviewProvider { - static var previews: some View { - - Group { - AsyncMultiplexImage( - multiplexImage: .init(identifier: "", urlsProvider: { size in - buildURLs( - baseURLString: - "https://images.unsplash.com/photo-1492446845049-9c50cc313f00?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8", - size: size - ) - }), - downloader: AsyncMultiplexImageNukeDownloader(pipeline: .shared, debugDelay: 2) - ) { phase in - switch phase { - case .empty: - Rectangle() - .foregroundColor(.yellow) - .overlay(Text("Loading")) - case .progress(let image): - image - .resizable() - .scaledToFill() - .overlay(Text("Progress")) - - case .success(let image): - image - .resizable() - .scaledToFill() - .overlay(Text("Done")) - case .failure(let error): - Text("Error") - } - } - .frame(width: 300, height: 300) - .overlay(Color.red.opacity(0.3)) - } - } -} - -struct BookAlign: View, PreviewProvider { - var body: some View { - if #available(iOS 15, *) { - Content() - } - } - - static var previews: some View { - Self() - } - - @available(iOS 15, *) - private struct Content: View { - - var body: some View { - ZStack { - AsyncImage( - url: .init( - string: - "https://images.unsplash.com/photo-1492446845049-9c50cc313f00?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8" - )! - ) { phase in - switch phase { - case .empty: - Rectangle() - .foregroundColor(.yellow) - .overlay(Text("Loading")) - case .success(let image): - image - .resizable() - .scaledToFill() - .overlay(Text("Done")) - case .failure(let error): - Text("Error") - @unknown default: - EmptyView() - } - } - } - .frame(width: 200, height: 200) - .overlay(Color.red.opacity(0.3)) - } - } -}