From 2936f86aadc6c609231c207c835a8fea1d8ae32b Mon Sep 17 00:00:00 2001 From: Muukii Date: Sun, 31 Mar 2024 18:44:31 +0900 Subject: [PATCH 1/4] WIP --- .../project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/swiftpm/Package.resolved | 0 .../AsyncMultiplexImage-Demo.xcscheme | 0 .../AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../AsyncMultiplexImage_Demo.entitlements | 0 .../ContentView.swift | 0 .../Preview Assets.xcassets/Contents.json | 0 .../AsyncMultiplexImageNukeDownloader.swift | 4 +- .../AsyncMultiplexImage.swift | 15 +-- .../AsyncMultiplexImageView.swift | 113 ++++++++++++++++++ 15 files changed, 123 insertions(+), 9 deletions(-) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo.xcodeproj/project.pbxproj (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo.xcodeproj/xcshareddata/xcschemes/AsyncMultiplexImage-Demo.xcscheme (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo/AppDelegate.swift (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo/Assets.xcassets/Contents.json (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo/AsyncMultiplexImage_Demo.entitlements (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo/ContentView.swift (100%) rename {AsyncMultiplexImage-Demo => Development}/AsyncMultiplexImage-Demo/Preview Content/Preview Assets.xcassets/Contents.json (100%) create mode 100644 Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/project.pbxproj b/Development/AsyncMultiplexImage-Demo.xcodeproj/project.pbxproj similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/project.pbxproj rename to Development/AsyncMultiplexImage-Demo.xcodeproj/project.pbxproj diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename to Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/xcshareddata/xcschemes/AsyncMultiplexImage-Demo.xcscheme b/Development/AsyncMultiplexImage-Demo.xcodeproj/xcshareddata/xcschemes/AsyncMultiplexImage-Demo.xcscheme similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo.xcodeproj/xcshareddata/xcschemes/AsyncMultiplexImage-Demo.xcscheme rename to Development/AsyncMultiplexImage-Demo.xcodeproj/xcshareddata/xcschemes/AsyncMultiplexImage-Demo.xcscheme diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/AppDelegate.swift b/Development/AsyncMultiplexImage-Demo/AppDelegate.swift similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/AppDelegate.swift rename to Development/AsyncMultiplexImage-Demo/AppDelegate.swift diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/Assets.xcassets/AccentColor.colorset/Contents.json b/Development/AsyncMultiplexImage-Demo/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/Assets.xcassets/AccentColor.colorset/Contents.json rename to Development/AsyncMultiplexImage-Demo/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Development/AsyncMultiplexImage-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Development/AsyncMultiplexImage-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/Assets.xcassets/Contents.json b/Development/AsyncMultiplexImage-Demo/Assets.xcassets/Contents.json similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/Assets.xcassets/Contents.json rename to Development/AsyncMultiplexImage-Demo/Assets.xcassets/Contents.json diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/AsyncMultiplexImage_Demo.entitlements b/Development/AsyncMultiplexImage-Demo/AsyncMultiplexImage_Demo.entitlements similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/AsyncMultiplexImage_Demo.entitlements rename to Development/AsyncMultiplexImage-Demo/AsyncMultiplexImage_Demo.entitlements diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/ContentView.swift b/Development/AsyncMultiplexImage-Demo/ContentView.swift similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/ContentView.swift rename to Development/AsyncMultiplexImage-Demo/ContentView.swift diff --git a/AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/Preview Content/Preview Assets.xcassets/Contents.json b/Development/AsyncMultiplexImage-Demo/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from AsyncMultiplexImage-Demo/AsyncMultiplexImage-Demo/Preview Content/Preview Assets.xcassets/Contents.json rename to Development/AsyncMultiplexImage-Demo/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift b/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift index defef7a..cd66e51 100644 --- a/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift +++ b/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift @@ -17,7 +17,7 @@ public struct AsyncMultiplexImageNukeDownloader: AsyncMultiplexImageDownloader { } public func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws - -> Image + -> UIImage { #if DEBUG @@ -41,7 +41,7 @@ public struct AsyncMultiplexImageNukeDownloader: AsyncMultiplexImageDownloader { ) ) - return .init(uiImage: response) + return response } } diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift index 9329425..b29a4d9 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift @@ -71,7 +71,7 @@ public final class DownloadManager { public protocol AsyncMultiplexImageDownloader { - func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws -> Image + func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws -> UIImage } public enum AsyncMultiplexImagePhase { @@ -100,7 +100,7 @@ public struct MultiplexImage: Hashable { public let identifier: String - fileprivate private(set) var _urlsProvider: @MainActor (CGSize) -> [URL] + private(set) var _urlsProvider: @MainActor (CGSize) -> [URL] public init( identifier: String, @@ -152,7 +152,7 @@ public struct AsyncMultiplexImage? @@ -172,6 +172,7 @@ private final class _AsyncMultiplexImageViewModel: ObservableObject { guard task.isCancelled == false else { return } task.cancel() } + } private struct _AsyncMultiplexImage: View @@ -216,9 +217,9 @@ private struct _AsyncMultiplexImage 0 && newSize.width > 0 else { + return + } + + if clearsContentBeforeDownload { + self.image = nil + } + + // making new candidates + let urls = image._urlsProvider(newSize) + + let candidates = urls.enumerated().map { i, e in + AsyncMultiplexImageCandidate(index: i, urlRequest: .init(url: e)) + } + + // start download + + let currentTask = Task { @MainActor [downloader] in + // this instance will be alive until finish + let container = ResultContainer() + let stream = await container.make( + candidates: candidates, + downloader: downloader, + displaySize: newSize + ) + + do { + for try await item in stream { + + // TODO: + + switch item { + case .progress(let image): + self.image = image + case .final(let image): + self.image = image + } + + } + } catch { + // FIXME: Error handling + } + } + + viewModel.registerCurrentTask(currentTask) + } + +} + +#endif From 1b3c92aed18e0770f2a2afe0b349c480e738e404 Mon Sep 17 00:00:00 2001 From: Muukii Date: Sun, 31 Mar 2024 18:45:42 +0900 Subject: [PATCH 2/4] Update --- Development/AsyncMultiplexImage-Demo/ContentView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Development/AsyncMultiplexImage-Demo/ContentView.swift b/Development/AsyncMultiplexImage-Demo/ContentView.swift index 65ed9e4..061c3cd 100644 --- a/Development/AsyncMultiplexImage-Demo/ContentView.swift +++ b/Development/AsyncMultiplexImage-Demo/ContentView.swift @@ -20,7 +20,7 @@ struct _SlowDownloader: AsyncMultiplexImageDownloader { self.pipeline = pipeline } - func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws -> Image { + func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws -> UIImage { switch candidate.index { case 0: @@ -36,7 +36,7 @@ struct _SlowDownloader: AsyncMultiplexImageDownloader { } let response = try await pipeline.image(for: .init(urlRequest: candidate.urlRequest)) - return .init(uiImage: response) + return response } } From 89fc8df8046a7947c3b2d697c2439b37be48cd13 Mon Sep 17 00:00:00 2001 From: Muukii Date: Sun, 31 Mar 2024 19:25:15 +0900 Subject: [PATCH 3/4] Update --- .../project.pbxproj | 39 ++++ .../xcshareddata/swiftpm/Package.resolved | 25 ++- .../ContentView.swift | 194 ++++++++++++++---- .../AsyncMultiplexImage.swift | 7 +- .../AsyncMultiplexImageView.swift | 54 ++++- 5 files changed, 263 insertions(+), 56 deletions(-) diff --git a/Development/AsyncMultiplexImage-Demo.xcodeproj/project.pbxproj b/Development/AsyncMultiplexImage-Demo.xcodeproj/project.pbxproj index e3aafcd..2761191 100644 --- a/Development/AsyncMultiplexImage-Demo.xcodeproj/project.pbxproj +++ b/Development/AsyncMultiplexImage-Demo.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 4B57106128D0AC8000AA053C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B57106028D0AC8000AA053C /* Preview Assets.xcassets */; }; 4B57106A28D0ACA300AA053C /* AsyncMultiplexImage in Frameworks */ = {isa = PBXBuildFile; productRef = 4B57106928D0ACA300AA053C /* AsyncMultiplexImage */; }; 4B57106C28D0ACA800AA053C /* AsyncMultiplexImage-Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 4B57106B28D0ACA800AA053C /* AsyncMultiplexImage-Nuke */; }; + 4B5AC66A2BB96A8D00641C3B /* SwiftUIHosting in Frameworks */ = {isa = PBXBuildFile; productRef = 4B5AC6692BB96A8D00641C3B /* SwiftUIHosting */; }; + 4B5AC66D2BB96AAB00641C3B /* MondrianLayout in Frameworks */ = {isa = PBXBuildFile; productRef = 4B5AC66C2BB96AAB00641C3B /* MondrianLayout */; }; 4B7E24F9296184E300E53388 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7E24F8296184E300E53388 /* AppDelegate.swift */; }; /* End PBXBuildFile section */ @@ -30,6 +32,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4B5AC66D2BB96AAB00641C3B /* MondrianLayout in Frameworks */, + 4B5AC66A2BB96A8D00641C3B /* SwiftUIHosting in Frameworks */, 4B57106A28D0ACA300AA053C /* AsyncMultiplexImage in Frameworks */, 4B57106C28D0ACA800AA053C /* AsyncMultiplexImage-Nuke in Frameworks */, ); @@ -102,6 +106,8 @@ packageProductDependencies = ( 4B57106928D0ACA300AA053C /* AsyncMultiplexImage */, 4B57106B28D0ACA800AA053C /* AsyncMultiplexImage-Nuke */, + 4B5AC6692BB96A8D00641C3B /* SwiftUIHosting */, + 4B5AC66C2BB96AAB00641C3B /* MondrianLayout */, ); productName = "AsyncMultiplexImage-Demo"; productReference = 4B57105528D0AC7F00AA053C /* AsyncMultiplexImage-Demo.app */; @@ -131,6 +137,10 @@ Base, ); mainGroup = 4B57104C28D0AC7F00AA053C; + packageReferences = ( + 4B5AC6682BB96A8D00641C3B /* XCRemoteSwiftPackageReference "swiftui-hosting" */, + 4B5AC66B2BB96AAB00641C3B /* XCRemoteSwiftPackageReference "MondrianLayout" */, + ); productRefGroup = 4B57105628D0AC7F00AA053C /* Products */; projectDirPath = ""; projectRoot = ""; @@ -369,6 +379,25 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + 4B5AC6682BB96A8D00641C3B /* XCRemoteSwiftPackageReference "swiftui-hosting" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/FluidGroup/swiftui-hosting.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.0; + }; + }; + 4B5AC66B2BB96AAB00641C3B /* XCRemoteSwiftPackageReference "MondrianLayout" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/FluidGroup/MondrianLayout.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.10.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 4B57106928D0ACA300AA053C /* AsyncMultiplexImage */ = { isa = XCSwiftPackageProductDependency; @@ -378,6 +407,16 @@ isa = XCSwiftPackageProductDependency; productName = "AsyncMultiplexImage-Nuke"; }; + 4B5AC6692BB96A8D00641C3B /* SwiftUIHosting */ = { + isa = XCSwiftPackageProductDependency; + package = 4B5AC6682BB96A8D00641C3B /* XCRemoteSwiftPackageReference "swiftui-hosting" */; + productName = SwiftUIHosting; + }; + 4B5AC66C2BB96AAB00641C3B /* MondrianLayout */ = { + isa = XCSwiftPackageProductDependency; + package = 4B5AC66B2BB96AAB00641C3B /* XCRemoteSwiftPackageReference "MondrianLayout" */; + productName = MondrianLayout; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4B57104D28D0AC7F00AA053C /* Project object */; diff --git a/Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f35381f..05ce6f6 100644 --- a/Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Development/AsyncMultiplexImage-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,21 +1,30 @@ { "pins" : [ + { + "identity" : "mondrianlayout", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FluidGroup/MondrianLayout.git", + "state" : { + "revision" : "5f00b13984fe08316fc5b5be06e2f41c14a3befa", + "version" : "0.10.0" + } + }, { "identity" : "nuke", "kind" : "remoteSourceControl", "location" : "https://github.com/kean/Nuke.git", "state" : { - "revision" : "c3864b8882bc69f5edfe5c70e18786c91d228b28", - "version" : "12.1.3" + "revision" : "4625c73ea00a9fb4b4f3e28d95d0021a44af7e59", + "version" : "12.5.0" } }, { - "identity" : "swiftui-gesture-velocity", + "identity" : "swiftui-hosting", "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/swiftui-gesture-velocity", + "location" : "https://github.com/FluidGroup/swiftui-hosting.git", "state" : { - "revision" : "9c83f8995f9e5efc29db2fca4b9ff058283f1603", - "version" : "1.0.0" + "revision" : "7e8eaca72eae910d6d3b6272c263c6c3a10b755c", + "version" : "1.2.0" } }, { @@ -23,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FluidGroup/swiftui-support.git", "state" : { - "revision" : "8ef53190c33bd345e7a95ef504dafe0f85ad9c4d", - "version" : "0.4.1" + "revision" : "c610c1e46c14c4660beb4ef7fe0241941dbecdc6", + "version" : "0.5.0" } } ], diff --git a/Development/AsyncMultiplexImage-Demo/ContentView.swift b/Development/AsyncMultiplexImage-Demo/ContentView.swift index 061c3cd..03eb0d7 100644 --- a/Development/AsyncMultiplexImage-Demo/ContentView.swift +++ b/Development/AsyncMultiplexImage-Demo/ContentView.swift @@ -5,22 +5,24 @@ // Created by Muukii on 2022/09/13. // -import SwiftUI - import AsyncMultiplexImage import AsyncMultiplexImage_Nuke -import SwiftUI +import MondrianLayout import Nuke +import SwiftUI +import SwiftUIHosting struct _SlowDownloader: AsyncMultiplexImageDownloader { let pipeline: ImagePipeline - + init(pipeline: ImagePipeline) { self.pipeline = pipeline } - - func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws -> UIImage { + + func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws + -> UIImage + { switch candidate.index { case 0: @@ -34,53 +36,165 @@ struct _SlowDownloader: AsyncMultiplexImageDownloader { default: break } - + let response = try await pipeline.image(for: .init(urlRequest: candidate.urlRequest)) return response } - + } struct ContentView: View { - - @State private var basePhotoURLString: String = "https://images.unsplash.com/photo-1492446845049-9c50cc313f00" - + + @State private var basePhotoURLString: String = + "https://images.unsplash.com/photo-1492446845049-9c50cc313f00" + var body: some View { - VStack { - AsyncMultiplexImage( - multiplexImage: .init(identifier: basePhotoURLString, urls: buildURLs(basePhotoURLString)), - downloader: _SlowDownloader(pipeline: .shared) - ) { phase in - switch phase { - case .empty: - Text("Loading") - case .progress(let image): - image - .resizable() - .scaledToFill() - case .success(let image): - image - .resizable() - .scaledToFill() - case .failure(let error): - Text("Error") + NavigationView { + Form { + Section { + NavigationLink("SwiftUI") { + VStack { + AsyncMultiplexImage( + multiplexImage: .init( + identifier: basePhotoURLString, + urls: buildURLs(basePhotoURLString) + ), + downloader: _SlowDownloader(pipeline: .shared) + ) { phase in + switch phase { + case .empty: + Text("Loading") + case .progress(let image): + image + .resizable() + .scaledToFill() + case .success(let image): + image + .resizable() + .scaledToFill() + case .failure(let error): + Text("Error") + } + } + + HStack { + Button("1") { + basePhotoURLString = + "https://images.unsplash.com/photo-1660668377331-da480e5339a0" + } + Button("2") { + basePhotoURLString = + "https://images.unsplash.com/photo-1658214764191-b002b517e9e5" + } + Button("3") { + basePhotoURLString = + "https://images.unsplash.com/photo-1587126396803-be14d33e49cf" + } + } + } + .padding() + .navigationTitle("SwiftUI") + } + NavigationLink("UIKit") { + UIKitContentViewRepresentable() + } } + .navigationTitle("Multiplex Image") } - + } + } +} + +struct UIKitContentViewRepresentable: UIViewRepresentable { + + func makeUIView(context: Context) -> UIKitContentView { + .init() + } + + func updateUIView(_ uiView: UIKitContentView, context: Context) { + + } + +} + +final class UIKitContentView: UIView { + + private let imageView: AsyncMultiplexImageView = .init( + downloader: _SlowDownloader(pipeline: .shared), + clearsContentBeforeDownload: true + ) + + init() { + + super.init(frame: .null) + + let buttonsView = SwiftUIHostingView { [imageView] in HStack { Button("1") { - basePhotoURLString = "https://images.unsplash.com/photo-1660668377331-da480e5339a0" + + let basePhotoURLString = "https://images.unsplash.com/photo-1660668377331-da480e5339a0" + + imageView.setMultiplexImage( + .init( + identifier: basePhotoURLString, + urls: buildURLs(basePhotoURLString) + ) + ) + } Button("2") { - basePhotoURLString = "https://images.unsplash.com/photo-1658214764191-b002b517e9e5" + let basePhotoURLString = "https://images.unsplash.com/photo-1658214764191-b002b517e9e5" + + imageView.setMultiplexImage( + .init( + identifier: basePhotoURLString, + urls: buildURLs(basePhotoURLString) + ) + ) + } Button("3") { - basePhotoURLString = "https://images.unsplash.com/photo-1587126396803-be14d33e49cf" + let basePhotoURLString = "https://images.unsplash.com/photo-1587126396803-be14d33e49cf" + + imageView.setMultiplexImage( + .init( + identifier: basePhotoURLString, + urls: buildURLs(basePhotoURLString) + ) + ) } } } - .padding() + + Mondrian.buildSubviews(on: self) { + VStackBlock { + imageView + .viewBlock + .size(.init(width: 300, height: 300)) + buttonsView + } + } } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +@available(iOS 17, *)#Preview("UIKit"){ + let view = AsyncMultiplexImageView( + downloader: _SlowDownloader(pipeline: .shared), + clearsContentBeforeDownload: true + ) + view.setMultiplexImage( + .init( + identifier: "https://images.unsplash.com/photo-1660668377331-da480e5339a0", + urls: buildURLs("https://images.unsplash.com/photo-1660668377331-da480e5339a0") + ) + ) + view.frame = .init(origin: .zero, size: .init(width: 300, height: 300)) + return view } struct ContentView_Previews: PreviewProvider { @@ -90,20 +204,20 @@ struct ContentView_Previews: PreviewProvider { } func buildURLs(_ baseURLString: String) -> [URL] { - + var components = URLComponents(string: baseURLString)! - + return [ "", "w=100", "w=50", "w=10", ].map { - + components.query = $0 - + return components.url! - + } - + } diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift index b29a4d9..93175e8 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift @@ -58,8 +58,13 @@ extension OSLog { OSLog.init(subsystem: "app.muukii", category: "default") } static let view: OSLog = makeOSLogInDebug { - OSLog.init(subsystem: "app.muukii", category: "View") + OSLog.init(subsystem: "app.muukii", category: "SwiftUIVersion") } + + static let uiKit: OSLog = makeOSLogInDebug { + OSLog.init(subsystem: "app.muukii", category: "UIKitVersion") + } + } @MainActor diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift index 1058f1e..e208061 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift @@ -12,21 +12,39 @@ open class AsyncMultiplexImageView: UIImageView { private var currentUsingImage: MultiplexImage? private var currentUsingContentSize: CGSize? - private let clearsContentBeforeDownload: Bool + private var stashedImage: UIImage? = nil // MARK: - Initializers public init( downloader: any AsyncMultiplexImageDownloader, - clearsContentBeforeDownload: Bool = true + clearsContentBeforeDownload: Bool = true, + unloadsImageOnBackground: Bool = false ) { + self.downloader = downloader self.clearsContentBeforeDownload = clearsContentBeforeDownload super.init(frame: .null) + self.clipsToBounds = true self.contentMode = .scaleAspectFill + + NotificationCenter.default.addObserver( + self, + selector: #selector(didEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(willEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } @available(*, unavailable) @@ -34,6 +52,10 @@ open class AsyncMultiplexImageView: UIImageView { fatalError("init(coder:) has not been implemented") } + deinit { + Log.debug(.uiKit, "deinit \(self)") + } + // MARK: - Functions open override func layoutSubviews() { @@ -45,12 +67,19 @@ open class AsyncMultiplexImageView: UIImageView { } } + @objc + private func didEnterBackground() { + unloadImage() + } + + @objc + private func willEnterForeground() { + startDownload() + } + public func setMultiplexImage(_ image: MultiplexImage) { currentUsingImage = image - - if clearsContentBeforeDownload { - self.image = nil - } + startDownload() } private func startDownload() { @@ -108,6 +137,17 @@ open class AsyncMultiplexImageView: UIImageView { viewModel.registerCurrentTask(currentTask) } -} + private func unloadImage() { + weak var _image = self.image + self.image = nil + + #if DEBUG + if _image != nil { + Log.debug(.uiKit, "\(String(describing: _image)) was not deallocated afeter unload") + } + #endif + + } +} #endif From f5bded262642ffbb0f10187c835ecc76bf605b86 Mon Sep 17 00:00:00 2001 From: Muukii Date: Mon, 1 Apr 2024 01:14:09 +0900 Subject: [PATCH 4/4] Update --- .../ContentView.swift | 2 + .../AsyncMultiplexImageView.swift | 49 +++++++++++++------ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Development/AsyncMultiplexImage-Demo/ContentView.swift b/Development/AsyncMultiplexImage-Demo/ContentView.swift index 03eb0d7..fb5a0b3 100644 --- a/Development/AsyncMultiplexImage-Demo/ContentView.swift +++ b/Development/AsyncMultiplexImage-Demo/ContentView.swift @@ -128,6 +128,8 @@ final class UIKitContentView: UIView { super.init(frame: .null) + imageView.backgroundColor = .init(white: 0.5, alpha: 0.2) + let buttonsView = SwiftUIHostingView { [imageView] in HStack { Button("1") { diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift index e208061..6e64194 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift @@ -2,7 +2,7 @@ #if canImport(UIKit) import UIKit -open class AsyncMultiplexImageView: UIImageView { +open class AsyncMultiplexImageView: UIView { // MARK: - Properties @@ -13,7 +13,8 @@ open class AsyncMultiplexImageView: UIImageView { private var currentUsingImage: MultiplexImage? private var currentUsingContentSize: CGSize? private let clearsContentBeforeDownload: Bool - private var stashedImage: UIImage? = nil + + private let imageView: UIImageView = .init() // MARK: - Initializers @@ -28,8 +29,8 @@ open class AsyncMultiplexImageView: UIImageView { super.init(frame: .null) - self.clipsToBounds = true - self.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill NotificationCenter.default.addObserver( self, @@ -45,6 +46,15 @@ open class AsyncMultiplexImageView: UIImageView { object: nil ) + addSubview(imageView) + imageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: topAnchor), + imageView.bottomAnchor.constraint(equalTo: bottomAnchor), + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor) + ]) + } @available(*, unavailable) @@ -95,7 +105,7 @@ open class AsyncMultiplexImageView: UIImageView { } if clearsContentBeforeDownload { - self.image = nil + imageView.image = nil } // making new candidates @@ -107,7 +117,7 @@ open class AsyncMultiplexImageView: UIImageView { // start download - let currentTask = Task { @MainActor [downloader] in + let currentTask = Task { [downloader] in // this instance will be alive until finish let container = ResultContainer() let stream = await container.make( @@ -119,16 +129,25 @@ open class AsyncMultiplexImageView: UIImageView { do { for try await item in stream { - // TODO: - - switch item { - case .progress(let image): - self.image = image - case .final(let image): - self.image = image + // TODO: support custom animation + + await MainActor.run { + CATransaction.begin() + let transition = CATransition() + transition.duration = 0.13 + switch item { + case .progress(let image): + imageView.image = image + case .final(let image): + imageView.image = image + } + self.layer.add(transition, forKey: "transition") + CATransaction.commit() } } + + Log.debug(.uiKit, "download finished") } catch { // FIXME: Error handling } @@ -139,8 +158,8 @@ open class AsyncMultiplexImageView: UIImageView { private func unloadImage() { - weak var _image = self.image - self.image = nil + weak var _image = imageView.image + imageView.image = nil #if DEBUG if _image != nil {