Skip to content

Commit d0aa17a

Browse files
committed
Refine ImageDownloading with Options pattern and DownloadedImage output
- Add ImageDownloadingOptions and DownloadedImage types to ImageDownloading - Reorder CDNRequester, CDNStorage, and MediaLoader files: protocol at top, types at bottom - Update StreamMediaLoader and StreamImageDownloader implementations - Update test mocks
1 parent 7e412fc commit d0aa17a

8 files changed

Lines changed: 140 additions & 110 deletions

File tree

Sources/StreamChat/APIClient/CDNClient/CDNRequester.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ extension CDNRequester {
6565
}
6666
}
6767

68+
// MARK: - Options
69+
6870
/// Options for an image request through the CDN.
6971
public struct ImageRequestOptions: Sendable {
7072
/// Optional resize parameters for server-side resizing.
@@ -80,6 +82,8 @@ public struct FileRequestOptions: Sendable {
8082
public init() {}
8183
}
8284

85+
// MARK: - Result Types
86+
8387
/// The result of a CDN URL transformation, containing the final URL,
8488
/// optional HTTP headers, and an optional caching key.
8589
public struct CDNRequest: Sendable {

Sources/StreamChat/APIClient/CDNClient/CDNStorage.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ extension CDNStorage {
8686
}
8787
}
8888

89+
// MARK: - Options
90+
8991
/// Options for uploading an attachment to the CDN.
9092
public struct AttachmentUploadOptions: Sendable {
9193
/// A closure that broadcasts upload progress (0.0 to 1.0).

Sources/StreamChatCommonUI/ImageLoading/ImageDownloading.swift

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,52 @@ import UIKit
77
/// A thin abstraction over an image downloading pipeline (e.g. Nuke).
88
///
99
/// Each UI SDK provides its own conformance backed by its vendored image
10-
/// loading library. ``StreamImageLoader`` uses this protocol internally
10+
/// loading library. ``StreamMediaLoader`` uses this protocol internally
1111
/// so that `StreamChatCommonUI` never depends on Nuke directly.
1212
public protocol ImageDownloading: Sendable {
1313
/// Downloads an image from the given URL.
1414
///
1515
/// - Parameters:
1616
/// - url: The image URL to download.
17-
/// - headers: Optional HTTP headers to include in the request.
18-
/// - cachingKey: Optional caching key. If nil, the URL string is used.
19-
/// - resize: Optional target size for client-side resizing.
17+
/// - options: Options controlling headers, caching, and resizing.
2018
/// - completion: Called on the main actor with the downloaded image.
2119
func downloadImage(
2220
url: URL,
23-
headers: [String: String]?,
24-
cachingKey: String?,
25-
resize: CGSize?,
26-
completion: @escaping @MainActor (Result<UIImage, Error>) -> Void
21+
options: ImageDownloadingOptions,
22+
completion: @escaping @MainActor (Result<DownloadedImage, Error>) -> Void
2723
)
2824
}
25+
26+
// MARK: - Options
27+
28+
/// Options for downloading an image through ``ImageDownloading``.
29+
public struct ImageDownloadingOptions: Sendable {
30+
/// Optional HTTP headers to include in the request.
31+
public var headers: [String: String]?
32+
/// Optional caching key. If nil, the URL string is used.
33+
public var cachingKey: String?
34+
/// Optional target size for client-side resizing.
35+
public var resize: CGSize?
36+
37+
public init(
38+
headers: [String: String]? = nil,
39+
cachingKey: String? = nil,
40+
resize: CGSize? = nil
41+
) {
42+
self.headers = headers
43+
self.cachingKey = cachingKey
44+
self.resize = resize
45+
}
46+
}
47+
48+
// MARK: - Result Types
49+
50+
/// The result of downloading an image through ``ImageDownloading``.
51+
public struct DownloadedImage: Sendable {
52+
/// The downloaded image.
53+
public var image: UIImage
54+
55+
public init(image: UIImage) {
56+
self.image = image
57+
}
58+
}

Sources/StreamChatCommonUI/ImageLoading/MediaLoader.swift

Lines changed: 82 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -6,87 +6,6 @@ import AVKit
66
import StreamChat
77
import UIKit
88

9-
// MARK: - Options
10-
11-
/// Options for loading a single image through a ``MediaLoader``.
12-
public struct ImageLoadOptions: Sendable {
13-
/// Optional resize parameters for server-side resizing.
14-
public var resize: ImageResize?
15-
/// The CDN requester for URL transformation (signing, headers, resizing).
16-
public var cdnRequester: CDNRequester
17-
18-
public init(resize: ImageResize? = nil, cdnRequester: CDNRequester) {
19-
self.resize = resize
20-
self.cdnRequester = cdnRequester
21-
}
22-
}
23-
24-
/// Options for loading multiple images through a ``MediaLoader``.
25-
public struct ImageBatchLoadOptions: Sendable {
26-
/// Placeholder images used rotationally when a URL fails to load.
27-
public var placeholders: [UIImage]
28-
/// Whether to load thumbnail-sized versions of the images.
29-
public var loadThumbnails: Bool
30-
/// The desired thumbnail size in points.
31-
public var thumbnailSize: CGSize
32-
/// The CDN requester for URL transformation (signing, headers, resizing).
33-
public var cdnRequester: CDNRequester
34-
35-
public init(
36-
placeholders: [UIImage] = [],
37-
loadThumbnails: Bool = true,
38-
thumbnailSize: CGSize = CGSize(width: 40, height: 40),
39-
cdnRequester: CDNRequester
40-
) {
41-
self.placeholders = placeholders
42-
self.loadThumbnails = loadThumbnails
43-
self.thumbnailSize = thumbnailSize
44-
self.cdnRequester = cdnRequester
45-
}
46-
}
47-
48-
/// Options for loading video content through a ``MediaLoader``.
49-
public struct VideoLoadOptions: Sendable {
50-
/// The CDN requester for URL transformation (signing, headers).
51-
public var cdnRequester: CDNRequester
52-
53-
public init(cdnRequester: CDNRequester) {
54-
self.cdnRequester = cdnRequester
55-
}
56-
}
57-
58-
// MARK: - Result Types
59-
60-
/// The result of loading a single image through a ``MediaLoader``.
61-
public struct MediaLoaderImage: Sendable {
62-
/// The loaded image.
63-
public var image: UIImage
64-
65-
public init(image: UIImage) {
66-
self.image = image
67-
}
68-
}
69-
70-
/// The result of loading a video asset through a ``MediaLoader``.
71-
public struct MediaLoaderVideoAsset: Sendable {
72-
/// The video asset.
73-
public var asset: AVURLAsset
74-
75-
public init(asset: AVURLAsset) {
76-
self.asset = asset
77-
}
78-
}
79-
80-
/// The result of loading a video preview through a ``MediaLoader``.
81-
public struct MediaLoaderVideoPreview: Sendable {
82-
/// The preview thumbnail image.
83-
public var image: UIImage
84-
85-
public init(image: UIImage) {
86-
self.image = image
87-
}
88-
}
89-
909
/// A unified protocol for loading images and video previews.
9110
///
9211
/// Merges the responsibilities of image loading and video preview generation
@@ -95,7 +14,7 @@ public struct MediaLoaderVideoPreview: Sendable {
9514
///
9615
/// Configuration is passed via options structs on every call, so concrete
9716
/// implementations remain stateless with respect to CDN configuration.
98-
/// Changing the requester on `ChatClient` takes effect immediately without
17+
/// Changing the requester on `ChatClientConfig` takes effect immediately without
9918
/// recreating the loader.
10019
public protocol MediaLoader: AnyObject, Sendable {
10120
// MARK: - Image Loading
@@ -226,3 +145,84 @@ extension MediaLoader {
226145
}
227146
}
228147
}
148+
149+
// MARK: - Options
150+
151+
/// Options for loading a single image through a ``MediaLoader``.
152+
public struct ImageLoadOptions: Sendable {
153+
/// Optional resize parameters for server-side resizing.
154+
public var resize: ImageResize?
155+
/// The CDN requester for URL transformation (signing, headers, resizing).
156+
public var cdnRequester: CDNRequester
157+
158+
public init(resize: ImageResize? = nil, cdnRequester: CDNRequester) {
159+
self.resize = resize
160+
self.cdnRequester = cdnRequester
161+
}
162+
}
163+
164+
/// Options for loading multiple images through a ``MediaLoader``.
165+
public struct ImageBatchLoadOptions: Sendable {
166+
/// Placeholder images used rotationally when a URL fails to load.
167+
public var placeholders: [UIImage]
168+
/// Whether to load thumbnail-sized versions of the images.
169+
public var loadThumbnails: Bool
170+
/// The desired thumbnail size in points.
171+
public var thumbnailSize: CGSize
172+
/// The CDN requester for URL transformation (signing, headers, resizing).
173+
public var cdnRequester: CDNRequester
174+
175+
public init(
176+
placeholders: [UIImage] = [],
177+
loadThumbnails: Bool = true,
178+
thumbnailSize: CGSize = CGSize(width: 40, height: 40),
179+
cdnRequester: CDNRequester
180+
) {
181+
self.placeholders = placeholders
182+
self.loadThumbnails = loadThumbnails
183+
self.thumbnailSize = thumbnailSize
184+
self.cdnRequester = cdnRequester
185+
}
186+
}
187+
188+
/// Options for loading video content through a ``MediaLoader``.
189+
public struct VideoLoadOptions: Sendable {
190+
/// The CDN requester for URL transformation (signing, headers).
191+
public var cdnRequester: CDNRequester
192+
193+
public init(cdnRequester: CDNRequester) {
194+
self.cdnRequester = cdnRequester
195+
}
196+
}
197+
198+
// MARK: - Result Types
199+
200+
/// The result of loading a single image through a ``MediaLoader``.
201+
public struct MediaLoaderImage: Sendable {
202+
/// The loaded image.
203+
public var image: UIImage
204+
205+
public init(image: UIImage) {
206+
self.image = image
207+
}
208+
}
209+
210+
/// The result of loading a video asset through a ``MediaLoader``.
211+
public struct MediaLoaderVideoAsset: Sendable {
212+
/// The video asset.
213+
public var asset: AVURLAsset
214+
215+
public init(asset: AVURLAsset) {
216+
self.asset = asset
217+
}
218+
}
219+
220+
/// The result of loading a video preview through a ``MediaLoader``.
221+
public struct MediaLoaderVideoPreview: Sendable {
222+
/// The preview thumbnail image.
223+
public var image: UIImage
224+
225+
public init(image: UIImage) {
226+
self.image = image
227+
}
228+
}

Sources/StreamChatCommonUI/ImageLoading/StreamMediaLoader.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ open class StreamMediaLoader: MediaLoader, @unchecked Sendable {
5252
switch result {
5353
case let .success(cdnRequest):
5454
let resizeSize: CGSize? = options.resize.map { CGSize(width: $0.width, height: $0.height) }
55-
self?.downloader.downloadImage(
56-
url: cdnRequest.url,
55+
let downloadOptions = ImageDownloadingOptions(
5756
headers: cdnRequest.headers,
5857
cachingKey: cdnRequest.cachingKey,
5958
resize: resizeSize
60-
) { imageResult in
61-
completion(imageResult.map { MediaLoaderImage(image: $0) })
59+
)
60+
self?.downloader.downloadImage(url: cdnRequest.url, options: downloadOptions) { imageResult in
61+
completion(imageResult.map { MediaLoaderImage(image: $0.image) })
6262
}
6363
case let .failure(error):
6464
StreamConcurrency.onMain {

Sources/StreamChatUI/Utils/ImageLoading/StreamImageDownloader.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,32 @@ open class StreamImageDownloader: ImageDownloading, @unchecked Sendable {
1111

1212
open func downloadImage(
1313
url: URL,
14-
headers: [String: String]?,
15-
cachingKey: String?,
16-
resize: CGSize?,
17-
completion: @escaping @MainActor (Result<UIImage, Error>) -> Void
14+
options: ImageDownloadingOptions,
15+
completion: @escaping @MainActor (Result<DownloadedImage, Error>) -> Void
1816
) {
1917
var urlRequest = URLRequest(url: url)
20-
if let headers {
18+
if let headers = options.headers {
2119
for (key, value) in headers {
2220
urlRequest.setValue(value, forHTTPHeaderField: key)
2321
}
2422
}
2523

2624
var processors = [ImageProcessing]()
27-
if let resize, resize != .zero {
25+
if let resize = options.resize, resize != .zero {
2826
processors.append(ImageProcessors.Resize(size: resize))
2927
}
3028

3129
let request = ImageRequest(
3230
urlRequest: urlRequest,
3331
processors: processors,
34-
userInfo: cachingKey.map { [.imageIdKey: $0] }
32+
userInfo: options.cachingKey.map { [.imageIdKey: $0] }
3533
)
3634

3735
ImagePipeline.shared.loadImage(with: request) { result in
3836
StreamConcurrency.onMain {
3937
switch result {
4038
case let .success(imageResponse):
41-
completion(.success(imageResponse.image))
39+
completion(.success(DownloadedImage(image: imageResponse.image)))
4240
case let .failure(error):
4341
completion(.failure(error))
4442
}

Tests/StreamChatCommonUITests/ImageLoading/StreamImageLoader_Tests.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,8 @@ private final class MockCDNRequester: CDNRequester, @unchecked Sendable {
4040
private final class MockImageDownloader: ImageDownloading, @unchecked Sendable {
4141
func downloadImage(
4242
url: URL,
43-
headers: [String: String]?,
44-
cachingKey: String?,
45-
resize: CGSize?,
46-
completion: @escaping @MainActor (Result<UIImage, Error>) -> Void
43+
options: ImageDownloadingOptions,
44+
completion: @escaping @MainActor (Result<DownloadedImage, Error>) -> Void
4745
) {
4846
DispatchQueue.main.async {
4947
completion(.failure(NSError(domain: "MockImageDownloader", code: 0)))

Tests/StreamChatCommonUITests/ImageLoading/StreamVideoLoader_Tests.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@ final class StreamMediaLoader_VideoTests: XCTestCase {
1717
private final class MockImageDownloader: ImageDownloading, @unchecked Sendable {
1818
func downloadImage(
1919
url: URL,
20-
headers: [String: String]?,
21-
cachingKey: String?,
22-
resize: CGSize?,
23-
completion: @escaping @MainActor (Result<UIImage, Error>) -> Void
20+
options: ImageDownloadingOptions,
21+
completion: @escaping @MainActor (Result<DownloadedImage, Error>) -> Void
2422
) {
2523
DispatchQueue.main.async {
2624
completion(.failure(NSError(domain: "MockImageDownloader", code: 0)))

0 commit comments

Comments
 (0)