Skip to content

Commit 924c5d6

Browse files
keanmokagio
authored andcommitted
Feature Branch 25.7 (#23923)
* Rename ImageLoadingController * Add LightboxViewController to replace WPImageViewController * Integrate LightboxViewController in Reader * Add Media support in LightboxViewController * Add convenience init to LightboxViewController * Integrate LightboxViewController in SiteMedia * Integrate LightboxViewController in ReaderDetailsCoordinator (cover image) * INtegrate in DefaultContentCoordinator * Integrate LightboxViewController in Guteberg * Integrate LightboxViewController in ExternalMediaPickerViewController * Integrate LightboxViewController in PostSettingsViewController (featured image) * Remove FeaturedImageViewController (ObjC) * Rewrite PostFeaturedImageCell * Integrate LightboxViewController in ReaderCommentsViewController * Update WPRichTextImage to use AsyncImageView * Automatically pick thumbnail when available * Remove WPImageViewController * Update release notes * Remove ImageLoader * Remove ImageDimensionParser * Update MediaItemHeaderView to use AsyncImageView instead of CachedAnimatedImageView * Fix code formatting in RichTextView * Update AnimatedGifAttachmentViewProvider to use GIFImageView directly * Remove SolidColorActivityIndicator * Remove CachedAnimatedImageView * Remove GIFPlaybackStrategy * Update EditorMediaUtility to use ImageDownloader directly (without AuthenticatedImageDownload redirect) * Remove AuthenticatedImageDownload * Update MediaExternalExporter to use ImageDownloader for downloading GIF data * Remove AnimatedImageCache * Remove remaining AlamofireImage usages from the anouncement cells * Remove AlamofireImageCacheAdapter * Remove AlamofireImage * Add ImagePrefetcher * Update releaes notes * Add ImageRequest support in AsyncImageView * Add ImageSize * Fix an issue with blogging reminders flow not being shown after publishing a new post * Remove unused LightNavigationController * Remove BottomSheetViewController usage from BloggingReminders flow * Simplify BloggingRemindersFlowIntroViewController * Add SpacerView * Add BottomToolbarView * Fix notice covering the blogging reminders fow * Replace FancyButton * Add close button to BloggingRemindersFlowSettingsViewController * Fix BloggingRemindersTimeSelectionViewController presentation * Remove FancyButton from BloggingRemindersPushPromptViewController * Remove dismiss button (it now shows back) * Update BloggingRemindersPushPromptViewController layout * Remove FancyButton from BloggingRemindersFlowCompletionViewController * Update BloggingRemindersFlowCompletionViewController layout * Update releaes notes * Fix typo in release notes * Fix compliance popover accessibility settings * Fix an issue with compliance popover not dismissing * Update release notes * Remove unused CircularProgressView extensions * Remove BottomSheetViewController usage from JetpackBrandingCoordinator * Remove ottomSheetViewControllerTests * Remove BottomSheetViewController * Remove DrawerPresentationController * Update release notes * Add Share action to the site link on dashboard * Remove duplicated Share actions * Remove duplicated Strings.ok * Update release notes * Fix layout issues in Privacy Settings * Add assertion * Update release notes * Rename WordPressMedia to AsyncImageKit * Remove MediaHost from AsyncImageKit * Move ImageDownloader.shared to AsyncImageKit * Move AsyncImageView and other related types to AsyncImageKit * Fix unit tests * Cleanup MediaHost initializers * Optimize account lookup * Fix MediaHostTests * Fix crash in ReaderDetailFeaturedImageView * Fix RTL support in WebKitViewController * Use semantic back/forward chevrons in other places * Update StatsBaseCell * Update SiteStatsTableHeaderView * Replace disclosure-chevron and editor-chevron-left * Fix remainig incorrect chevron usages * Remove remainig chevron images * Update release notes * Fix separator insets on homepage * Fix an issue with clear navigation bar background in revision browser * Fix an issue with clear navigation bar background in revision browser * Fix toolbar inset to safe area in revision browser * Modernize menus and stuff * Fix MediaRequestAuthenticatorTests * Remove preflight connection check when sending replies (can be lagging behind) * Fix an issue with comments disppearing if request fails * Update other screens using TextView * Update release notes * Fix formatting * Fix an issue with referrers showing invalid icons * Update release notes * Remove some of the scenarios where isInternetConnected used * Update site menu style on iPhone * Update release notes * Integrate zoom transitions in Theme browser * Update release notes * Fix tint colors in wpios * Remove UIAppColor.brand * Enable zoom transitions in Reader (iPad) * Update release notes * Remove unused isVisibleInScrollView * Enable toolbar hiding on iPad * Fix ReaderDetailFeaturedImageView gradienet showing up when no image is present * Fix an issue with Publisize options appearing in the prepublishing sheet for XMLRPC sites * Fix code formatting and remove unused imports * Move SiteIconView to WordPressUI * Update Share extension to use SiteIconView * Remove UIImageView+Blavatar * Use firstLetter * Update release notes * Disable universal links support for QR code login * Fix an issue with the confirmation screen shown more than once * Update release notes * Enable fast deceleration for filters on the Discover tab * Update release notes * Show selected filter in the Discover navigation bar * Update release notes * Remove unused makeCreateButtonAnnouncementAlertController * Add scroll-to-top button to Reader * Cleanup * Update design for iPad * Add analytics * Update releaes notes * Flatten a nested localized string to avoid `genstrings` failure * Import `WordPressUI` in `SiteIconViewModelTests` Otherwise, it won't build * Add initial MediaPicker implementation * Add initial PostSettingsFeaturedImageCell implementation * Add configurable MediaPicker content * Add ViewModel to PostSettingsFeaturedImageCell * Add reuseIdentifier for featured image cells * Pass selection from MediaPicker to PostSettingsFeaturedImageViewModel * Show upload status using PostMediaUploadItemView * Rename MediaUploadItemViewModel * Add PostSettingsFeaturedImageUploadView to show upload progress * Simlify how the app shows media upload status * Handle upload failure * Implement featured image save * Add support for showing a selected featured image * Add support for removing featured image * Simplify lightbox * Add support for camera as a source * Add .siteMedia(blog:) source * Add ImagePlayground source support * Add ImagePlayground support in MediaPicker * Add free photos and GIFs support to MediaPicker * Remove unused media upload code from PostSettingsViewController * Remove WPTableViewActivityCell * Remove WPProgressTableViewCell * Remvove unused featured image size * Remove more unused code * Remove unused code * Add SiteMediaImageView * Remove unused code * Integrate FeaturedImageDelegate * Fix SiteMediaImage background when loading with spinner * Fix animations * Add zoom transition * Add shadow to more menu * Make the entire cell tappable * Add View action * Add replace action * Show spinner when replacing an image * Remove unused reloadFeaturedImageCell * Update release notse * Revert "Update site menu style on iPhone" This reverts commit 565a34b. * Fix an issue with wrong cover images appearing in Reader (#23914) * Fix an issue with wrong cover images appearing in Reader * Update release notes * Update release notes * Point back to wpios-edition * Fix an issue with non-stable order in Posts and Pages in stats (#23915) * Fix an issue with non-stable order in Posts and Pages in stats * Update release notes * Update release notes * Fix an issue with a missing "Mark as Unread" button in the More menu (#23917) * Add missing toggle read/unread button * Show read status in the list * Update release notes * Update release notes * Add missing social sharing icons (#23918) * Add missing social sharing icons * Update release notes * Update release notes * Update release notes * Fix build * Update UI tests * Use medium font for main navigation area in Reader to align with Home * Remove commented-out code * Add context menus and previews for sites in Reader (#23964) * Fix l10n typo * Add Unsubscribe context menu to Reader sidebar sites * Extract ReaderSiteFavoriteButton * Move actions to ReaderSidebarSubscriptionCell * Extract ReaderSubscriptionContextMenu and add Share * Add Notification Settings and Copy Link buttons * Add context menu for sites in Subscriptions view * Add previews * Fix notification settings sometimes being clipped on iPad * Fix layout in ReaderSubscriptionCel actions * Update release notes * Fix more button color in dark mode * Fix an issue with fullscreen button in reply view clipped by the notch (#23965) * Fix an issue with fullscreen button in reply view clipped by the notch * Update release notes * Fix display of certain topics in Discover recommendations * Remove "Lazy Images" Jetpack option (#23966) * Remove lazy loading: * Add error handling * Update release notes * Update WordPressKit (has lazy-load fix) * Update release notes * Reoder site actions * Update release notes * Add missing imports --------- Co-authored-by: Gio Lodi <[email protected]>
1 parent 0cc00b2 commit 924c5d6

File tree

342 files changed

+3941
-7308
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

342 files changed

+3941
-7308
lines changed

Modules/Package.swift

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ let package = Package(
88
.iOS(.v16),
99
],
1010
products: XcodeSupport.products + [
11-
.library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]),
11+
.library(name: "AsyncImageKit", targets: ["AsyncImageKit"]),
1212
.library(name: "DesignSystem", targets: ["DesignSystem"]),
13+
.library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]),
1314
.library(name: "WordPressFlux", targets: ["WordPressFlux"]),
14-
.library(name: "WordPressMedia", targets: ["WordPressMedia"]),
1515
.library(name: "WordPressShared", targets: ["WordPressShared"]),
1616
.library(name: "WordPressUI", targets: ["WordPressUI"]),
1717
],
1818
dependencies: [
1919
.package(url: "https://github.com/airbnb/lottie-ios", from: "4.4.0"),
2020
.package(url: "https://github.com/Alamofire/Alamofire", from: "5.9.1"),
21-
.package(url: "https://github.com/Alamofire/AlamofireImage", from: "4.3.0"),
2221
.package(url: "https://github.com/AliSoftware/OHHTTPStubs", from: "9.1.0"),
22+
.package(url: "https://github.com/apple/swift-collections", from: "1.0.0"),
2323
.package(url: "https://github.com/Automattic/Automattic-Tracks-iOS", from: "3.4.2"),
2424
.package(url: "https://github.com/Automattic/AutomatticAbout-swift", from: "1.1.4"),
2525
.package(url: "https://github.com/Automattic/Gravatar-SDK-iOS", from: "3.1.1"),
@@ -52,23 +52,34 @@ let package = Package(
5252
.package(url: "https://github.com/Automattic/color-studio", branch: "trunk"),
5353
],
5454
targets: XcodeSupport.targets + [
55-
.target(name: "JetpackStatsWidgetsCore", swiftSettings: [.swiftLanguageMode(.v5)]),
55+
.target(name: "AsyncImageKit", dependencies: [
56+
.product(name: "Collections", package: "swift-collections"),
57+
.product(name: "Gifu", package: "Gifu"),
58+
]),
5659
.target(name: "DesignSystem", swiftSettings: [.swiftLanguageMode(.v5)]),
60+
.target(name: "JetpackStatsWidgetsCore", swiftSettings: [.swiftLanguageMode(.v5)]),
5761
.target(name: "UITestsFoundation", dependencies: [
5862
.product(name: "ScreenObject", package: "ScreenObject"),
5963
.product(name: "XCUITestHelpers", package: "XCUITestHelpers"),
6064
], swiftSettings: [.swiftLanguageMode(.v5)]),
6165
.target(name: "WordPressFlux", swiftSettings: [.swiftLanguageMode(.v5)]),
62-
.target(name: "WordPressMedia"),
6366
.target(name: "WordPressSharedObjC", resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]),
6467
.target(name: "WordPressShared", dependencies: [.target(name: "WordPressSharedObjC")], resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]),
6568
.target(name: "WordPressTesting", resources: [.process("Resources")]),
66-
.target(name: "WordPressUI", dependencies: [.target(name: "WordPressShared")], resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]),
69+
.target(
70+
name: "WordPressUI",
71+
dependencies: [
72+
"AsyncImageKit",
73+
.target(name: "WordPressShared")
74+
],
75+
resources: [.process("Resources")],
76+
swiftSettings: [.swiftLanguageMode(.v5)]
77+
),
6778
.testTarget(name: "JetpackStatsWidgetsCoreTests", dependencies: [.target(name: "JetpackStatsWidgetsCore")], swiftSettings: [.swiftLanguageMode(.v5)]),
6879
.testTarget(name: "DesignSystemTests", dependencies: [.target(name: "DesignSystem")], swiftSettings: [.swiftLanguageMode(.v5)]),
6980
.testTarget(name: "WordPressFluxTests", dependencies: ["WordPressFlux"], swiftSettings: [.swiftLanguageMode(.v5)]),
70-
.testTarget(name: "WordPressMediaTests", dependencies: [
71-
.target(name: "WordPressMedia"),
81+
.testTarget(name: "AsyncImageKitTests", dependencies: [
82+
.target(name: "AsyncImageKit"),
7283
.target(name: "WordPressTesting"),
7384
.product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs")
7485
]),
@@ -143,10 +154,9 @@ enum XcodeSupport {
143154
"JetpackStatsWidgetsCore",
144155
"WordPressFlux",
145156
"WordPressShared",
146-
"WordPressMedia",
157+
"AsyncImageKit",
147158
"WordPressUI",
148159
.product(name: "Alamofire", package: "Alamofire"),
149-
.product(name: "AlamofireImage", package: "AlamofireImage"),
150160
.product(name: "AutomatticAbout", package: "AutomatticAbout-swift"),
151161
.product(name: "AutomatticTracks", package: "Automattic-Tracks-iOS"),
152162
.product(name: "CocoaLumberjack", package: "CocoaLumberjack"),
@@ -191,6 +201,7 @@ enum XcodeSupport {
191201
.xcodeTarget("XcodeTarget_StatsWidget", dependencies: [
192202
"JetpackStatsWidgetsCore",
193203
"WordPressShared",
204+
"WordPressUI",
194205
.product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"),
195206
.product(name: "WordPressAPI", package: "wordpress-rs"),
196207
.product(name: "ColorStudio", package: "color-studio"),

Modules/Sources/WordPressMedia/ImageDecoder.swift renamed to Modules/Sources/AsyncImageKit/Helpers/ImageDecoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ private extension Data {
7070
}
7171
}
7272

73-
private extension CGSize {
73+
extension CGSize {
7474
func scaled(by scale: CGFloat) -> CGSize {
7575
CGSize(width: width * scale, height: height * scale)
7676
}

Modules/Sources/WordPressMedia/ImageDownloader.swift renamed to Modules/Sources/AsyncImageKit/ImageDownloader.swift

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import UIKit
33
/// The system that downloads and caches images, and prepares them for display.
44
@ImageDownloaderActor
55
public final class ImageDownloader {
6+
public nonisolated static let shared = ImageDownloader()
7+
68
private nonisolated let cache: MemoryCacheProtocol
7-
private let authenticator: MediaRequestAuthenticatorProtocol?
89

910
private let urlSession = URLSession {
1011
$0.urlCache = nil
@@ -21,14 +22,12 @@ public final class ImageDownloader {
2122
private var tasks: [String: ImageDataTask] = [:]
2223

2324
public nonisolated init(
24-
cache: MemoryCacheProtocol = MemoryCache.shared,
25-
authenticator: MediaRequestAuthenticatorProtocol?
25+
cache: MemoryCacheProtocol = MemoryCache.shared
2626
) {
2727
self.cache = cache
28-
self.authenticator = authenticator
2928
}
3029

31-
public func image(from url: URL, host: MediaHost? = nil, options: ImageRequestOptions = .init()) async throws -> UIImage {
30+
public func image(from url: URL, host: MediaHostProtocol? = nil, options: ImageRequestOptions = .init()) async throws -> UIImage {
3231
try await image(for: ImageRequest(url: url, host: host, options: options))
3332
}
3433

@@ -39,7 +38,7 @@ public final class ImageDownloader {
3938
return image
4039
}
4140
let data = try await data(for: request)
42-
let image = try await ImageDecoder.makeImage(from: data, size: options.size)
41+
let image = try await ImageDecoder.makeImage(from: data, size: options.size.map(CGSize.init))
4342
if options.isMemoryCacheEnabled {
4443
cache[key] = image
4544
}
@@ -55,8 +54,8 @@ public final class ImageDownloader {
5554
switch request.source {
5655
case .url(let url, let host):
5756
var request: URLRequest
58-
if let host, let authenticator {
59-
request = try await authenticator.authenticatedRequest(for: url, host: host)
57+
if let host {
58+
request = try await host.authenticatedRequest(for: url)
6059
} else {
6160
request = URLRequest(url: url)
6261
}
@@ -69,24 +68,30 @@ public final class ImageDownloader {
6968

7069
// MARK: - Caching
7170

71+
/// Returns an image from the memory cache.
72+
nonisolated public func cachedImage(for request: ImageRequest) -> UIImage? {
73+
guard let imageURL = request.source.url else { return nil }
74+
return cachedImage(for: imageURL, size: request.options.size)
75+
}
76+
7277
/// Returns an image from the memory cache.
7378
///
7479
/// - note: Use it to retrieve the image synchronously, which is no not possible
7580
/// with the async functions.
76-
nonisolated public func cachedImage(for imageURL: URL, size: CGSize? = nil) -> UIImage? {
81+
nonisolated public func cachedImage(for imageURL: URL, size: ImageSize? = nil) -> UIImage? {
7782
cache[makeKey(for: imageURL, size: size)]
7883
}
7984

80-
nonisolated public func setCachedImage(_ image: UIImage?, for imageURL: URL, size: CGSize? = nil) {
85+
nonisolated public func setCachedImage(_ image: UIImage?, for imageURL: URL, size: ImageSize? = nil) {
8186
cache[makeKey(for: imageURL, size: size)] = image
8287
}
8388

84-
private nonisolated func makeKey(for imageURL: URL?, size: CGSize?) -> String {
89+
private nonisolated func makeKey(for imageURL: URL?, size: ImageSize?) -> String {
8590
guard let imageURL else {
8691
assertionFailure("The request.url was nil") // This should never happen
8792
return ""
8893
}
89-
return imageURL.absoluteString + (size.map { "?size=\($0)" } ?? "")
94+
return imageURL.absoluteString + (size.map { "?w=\($0.width),h=\($0.height)" } ?? "")
9095
}
9196

9297
public func clearURLSessionCache() {
@@ -189,6 +194,6 @@ private extension URLSession {
189194
}
190195
}
191196

192-
public protocol MediaRequestAuthenticatorProtocol: Sendable {
193-
@MainActor func authenticatedRequest(for url: URL, host: MediaHost) async throws -> URLRequest
197+
public protocol MediaHostProtocol: Sendable {
198+
@MainActor func authenticatedRequest(for url: URL) async throws -> URLRequest
194199
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import UIKit
2+
import Collections
3+
4+
@ImageDownloaderActor
5+
public final class ImagePrefetcher {
6+
private let downloader: ImageDownloader
7+
private let maxConcurrentTasks: Int
8+
private var queue = OrderedDictionary<PrefetchKey, PrefetchTask>()
9+
private var numberOfActiveTasks = 0
10+
11+
deinit {
12+
let tasks = queue.values.compactMap(\.task)
13+
for task in tasks {
14+
task.cancel()
15+
}
16+
}
17+
18+
public nonisolated init(
19+
downloader: ImageDownloader = .shared,
20+
maxConcurrentTasks: Int = 2
21+
) {
22+
self.downloader = downloader
23+
self.maxConcurrentTasks = maxConcurrentTasks
24+
}
25+
26+
public nonisolated func startPrefetching(for requests: [ImageRequest]) {
27+
Task { @ImageDownloaderActor in
28+
for request in requests {
29+
startPrefetching(for: request)
30+
}
31+
performPendingTasks()
32+
}
33+
}
34+
35+
private func startPrefetching(for request: ImageRequest) {
36+
let key = PrefetchKey(request: request)
37+
guard queue[key] == nil else {
38+
return
39+
}
40+
queue[key] = PrefetchTask()
41+
}
42+
43+
private func performPendingTasks() {
44+
var index = 0
45+
func nextPendingTask() -> (PrefetchKey, PrefetchTask)? {
46+
while index < queue.count {
47+
if queue.elements[index].value.task == nil {
48+
return queue.elements[index]
49+
}
50+
index += 1
51+
}
52+
return nil
53+
}
54+
while numberOfActiveTasks < maxConcurrentTasks, let (key, task) = nextPendingTask() {
55+
task.task = Task {
56+
await self.actuallyPrefetchImage(for: key.request)
57+
}
58+
numberOfActiveTasks += 1
59+
}
60+
}
61+
62+
private func actuallyPrefetchImage(for request: ImageRequest) async {
63+
_ = try? await downloader.image(for: request)
64+
65+
numberOfActiveTasks -= 1
66+
queue[PrefetchKey(request: request)] = nil
67+
performPendingTasks()
68+
}
69+
70+
public nonisolated func stopPrefetching(for requests: [ImageRequest]) {
71+
Task { @ImageDownloaderActor in
72+
for request in requests {
73+
stopPrefetching(for: request)
74+
}
75+
performPendingTasks()
76+
}
77+
}
78+
79+
private func stopPrefetching(for request: ImageRequest) {
80+
let key = PrefetchKey(request: request)
81+
if let task = queue.removeValue(forKey: key) {
82+
task.task?.cancel()
83+
}
84+
}
85+
86+
public nonisolated func stopAll() {
87+
Task { @ImageDownloaderActor in
88+
for (_, value) in queue {
89+
value.task?.cancel()
90+
}
91+
queue.removeAll()
92+
}
93+
}
94+
95+
private struct PrefetchKey: Hashable, Sendable {
96+
let request: ImageRequest
97+
98+
func hash(into hasher: inout Hasher) {
99+
request.source.url?.hash(into: &hasher)
100+
}
101+
102+
static func == (lhs: PrefetchKey, rhs: PrefetchKey) -> Bool {
103+
let (lhs, rhs) = (lhs.request, rhs.request)
104+
return (lhs.source.url, lhs.options) == (rhs.source.url, rhs.options)
105+
}
106+
}
107+
108+
private final class PrefetchTask: @unchecked Sendable {
109+
var task: Task<Void, Error>?
110+
}
111+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import UIKit
2+
3+
public final class ImageRequest: Sendable {
4+
public enum Source: Sendable {
5+
case url(URL, MediaHostProtocol?)
6+
case urlRequest(URLRequest)
7+
8+
var url: URL? {
9+
switch self {
10+
case .url(let url, _): url
11+
case .urlRequest(let request): request.url
12+
}
13+
}
14+
}
15+
16+
let source: Source
17+
let options: ImageRequestOptions
18+
19+
public init(url: URL, host: MediaHostProtocol? = nil, options: ImageRequestOptions = .init()) {
20+
self.source = .url(url, host)
21+
self.options = options
22+
}
23+
24+
public init(urlRequest: URLRequest, options: ImageRequestOptions = .init()) {
25+
self.source = .urlRequest(urlRequest)
26+
self.options = options
27+
}
28+
}
29+
30+
public struct ImageRequestOptions: Hashable, Sendable {
31+
/// Resize the thumbnail to the given size. By default, `nil`.
32+
public var size: ImageSize?
33+
34+
/// If enabled, uses ``MemoryCache`` for caching decompressed images.
35+
public var isMemoryCacheEnabled = true
36+
37+
/// If enabled, uses `URLSession` preconfigured with a custom `URLCache`
38+
/// with a relatively high disk capacity. By default, `true`.
39+
public var isDiskCacheEnabled = true
40+
41+
public init(
42+
size: ImageSize? = nil,
43+
isMemoryCacheEnabled: Bool = true,
44+
isDiskCacheEnabled: Bool = true
45+
) {
46+
self.size = size
47+
self.isMemoryCacheEnabled = isMemoryCacheEnabled
48+
self.isDiskCacheEnabled = isDiskCacheEnabled
49+
}
50+
}
51+
52+
/// Image size in **pixels**.
53+
public struct ImageSize: Hashable, Sendable {
54+
public let width: CGFloat
55+
public let height: CGFloat
56+
57+
public init(width: CGFloat, height: CGFloat) {
58+
self.width = width
59+
self.height = height
60+
}
61+
62+
public init(_ size: CGSize) {
63+
self.width = size.width
64+
self.height = size.height
65+
}
66+
67+
/// Initializes `ImageSize` with the given size scaled for the given view.
68+
@MainActor
69+
public init(scaling size: CGSize, in view: UIView) {
70+
self.init(size.scaled(by: view.traitCollection.displayScale))
71+
}
72+
73+
/// Initializes `ImageSize` with the given size scaled for the current trait
74+
/// collection display scale.
75+
public init(scaling size: CGSize) {
76+
self.init(size.scaled(by: UITraitCollection.current.displayScale))
77+
}
78+
}
79+
80+
extension CGSize {
81+
init(_ size: ImageSize) {
82+
self.init(width: size.width, height: size.height)
83+
}
84+
}

0 commit comments

Comments
 (0)