From 1802b0bdf3ec6f59ee42c074d7e0e6119c2b1c60 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Tue, 25 Oct 2022 13:05:08 +0100 Subject: [PATCH] Improve `downloadImage()` API with `ImageDownloadRequest` structure --- .../ChatMessageLinkPreviewView.swift | 6 +++- .../AvatarView/ChatChannelAvatarView.swift | 6 ++-- .../{ => ImageCDN}/ImageCDN.swift | 0 .../{ => ImageCDN}/StreamCDN.swift | 0 .../ImageLoading/ImageDownloadRequest.swift | 16 ++++++++++ .../Utils/ImageLoading/ImageLoading.swift | 30 +++++++------------ .../Utils/ImageLoading/NukeImageLoader.swift | 15 ++++++---- StreamChat.xcodeproj/project.pbxproj | 18 +++++++++-- .../Mocks/ImageLoader_Mock.swift | 19 +++++++----- 9 files changed, 71 insertions(+), 39 deletions(-) rename Sources/StreamChatUI/Utils/ImageLoading/{ => ImageCDN}/ImageCDN.swift (100%) rename Sources/StreamChatUI/Utils/ImageLoading/{ => ImageCDN}/StreamCDN.swift (100%) create mode 100644 Sources/StreamChatUI/Utils/ImageLoading/ImageDownloadRequest.swift diff --git a/Sources/StreamChatUI/ChatMessageList/Attachments/ChatMessageLinkPreviewView.swift b/Sources/StreamChatUI/ChatMessageList/Attachments/ChatMessageLinkPreviewView.swift index 10b01f7dbea..6422d376c1a 100644 --- a/Sources/StreamChatUI/ChatMessageList/Attachments/ChatMessageLinkPreviewView.swift +++ b/Sources/StreamChatUI/ChatMessageList/Attachments/ChatMessageLinkPreviewView.swift @@ -122,7 +122,11 @@ open class ChatMessageLinkPreviewView: _Control, ThemeProvider { authorLabel.textColor = tintColor - components.imageLoader.loadImage(into: imagePreview, from: payload?.previewURL) + components.imageLoader.loadImage( + into: imagePreview, + from: payload?.previewURL, + with: ImageLoaderOptions() + ) imagePreview.isHidden = isImageHidden authorLabel.text = payload?.author diff --git a/Sources/StreamChatUI/CommonViews/AvatarView/ChatChannelAvatarView.swift b/Sources/StreamChatUI/CommonViews/AvatarView/ChatChannelAvatarView.swift index 8fbe3206939..2ff510b3685 100644 --- a/Sources/StreamChatUI/CommonViews/AvatarView/ChatChannelAvatarView.swift +++ b/Sources/StreamChatUI/CommonViews/AvatarView/ChatChannelAvatarView.swift @@ -141,11 +141,11 @@ open class ChatChannelAvatarView: _View, ThemeProvider, SwiftUIRepresentable { } let avatarSize = components.avatarThumbnailSize - let urlsAndOptions = avatarUrls.map { - (url: $0, options: ImageDownloadOptions(resize: .init(avatarSize))) + let requests = avatarUrls.map { + ImageDownloadRequest(url: $0, options: ImageDownloadOptions(resize: .init(avatarSize))) } - components.imageLoader.downloadMultipleImages(from: urlsAndOptions) { results in + components.imageLoader.downloadMultipleImages(with: requests) { results in let imagesMapper = ImageResultsMapper(results: results) let images = imagesMapper.mapErrors(with: placeholderImages) completion(images, channelId) diff --git a/Sources/StreamChatUI/Utils/ImageLoading/ImageCDN.swift b/Sources/StreamChatUI/Utils/ImageLoading/ImageCDN/ImageCDN.swift similarity index 100% rename from Sources/StreamChatUI/Utils/ImageLoading/ImageCDN.swift rename to Sources/StreamChatUI/Utils/ImageLoading/ImageCDN/ImageCDN.swift diff --git a/Sources/StreamChatUI/Utils/ImageLoading/StreamCDN.swift b/Sources/StreamChatUI/Utils/ImageLoading/ImageCDN/StreamCDN.swift similarity index 100% rename from Sources/StreamChatUI/Utils/ImageLoading/StreamCDN.swift rename to Sources/StreamChatUI/Utils/ImageLoading/ImageCDN/StreamCDN.swift diff --git a/Sources/StreamChatUI/Utils/ImageLoading/ImageDownloadRequest.swift b/Sources/StreamChatUI/Utils/ImageLoading/ImageDownloadRequest.swift new file mode 100644 index 00000000000..436817d822c --- /dev/null +++ b/Sources/StreamChatUI/Utils/ImageLoading/ImageDownloadRequest.swift @@ -0,0 +1,16 @@ +// +// Copyright © 2022 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// The url and options information of an image download request. +public struct ImageDownloadRequest { + public let url: URL + public let options: ImageDownloadOptions + + public init(url: URL, options: ImageDownloadOptions) { + self.url = url + self.options = options + } +} diff --git a/Sources/StreamChatUI/Utils/ImageLoading/ImageLoading.swift b/Sources/StreamChatUI/Utils/ImageLoading/ImageLoading.swift index 88fc0319e0b..98f09f58979 100644 --- a/Sources/StreamChatUI/Utils/ImageLoading/ImageLoading.swift +++ b/Sources/StreamChatUI/Utils/ImageLoading/ImageLoading.swift @@ -24,24 +24,22 @@ public protocol ImageLoading: AnyObject { /// Download an image from the given `URL`. /// - Parameters: - /// - url: The `URL` of the image. - /// - options: The loading options on how to fetch the image. + /// - request: The url and options information of an image download request. /// - completion: The completion when the loading is finished. /// - Returns: A cancellable task. @discardableResult func downloadImage( - from url: URL, - with options: ImageDownloadOptions, + with request: ImageDownloadRequest, completion: @escaping ((_ result: Result) -> Void) ) -> Cancellable? /// Load a batch of images and get notified when all of them complete loading. /// - Parameters: - /// - urlsAndOptions: A tuple of urls and the options on how to fetch the image. + /// - requests: The urls and options information of each image download request. /// - completion: The completion when the loading is finished. /// It returns an array of image and errors in case the image failed to load. func downloadMultipleImages( - from urlsAndOptions: [(url: URL, options: ImageDownloadOptions)], + with requests: [ImageDownloadRequest], completion: @escaping (([Result]) -> Void) ) @@ -93,6 +91,7 @@ public extension ImageLoading { return loadImage( into: imageView, from: attachmentPayload?.imageURL, + with: ImageLoaderOptions(), completion: completion ) } @@ -116,20 +115,12 @@ public extension ImageLoading { // MARK: - Default Parameters public extension ImageLoading { - @discardableResult - func downloadImage( - from url: URL, - with options: ImageDownloadOptions = .init(), - completion: @escaping ((_ result: Result) -> Void) - ) -> Cancellable? { - downloadImage(from: url, with: options, completion: completion) - } - + // Default empty completion block. @discardableResult func loadImage( into imageView: UIImageView, from url: URL?, - with options: ImageLoaderOptions = .init(), + with options: ImageLoaderOptions, completion: ((_ result: Result) -> Void)? = nil ) -> Cancellable? { loadImage(into: imageView, from: url, with: options, completion: completion) @@ -150,8 +141,7 @@ public extension ImageLoading { } return downloadImage( - from: url, - with: ImageDownloadOptions(resize: nil), + with: ImageDownloadRequest(url: url, options: ImageDownloadOptions()), completion: completion ) } @@ -188,10 +178,10 @@ public extension ImageLoading { completion: @escaping (([UIImage]) -> Void) ) { let urlsAndOptions = urls.map { url in - (url: url, options: ImageDownloadOptions(resize: .init(thumbnailSize))) + ImageDownloadRequest(url: url, options: .init(resize: .init(thumbnailSize))) } - downloadMultipleImages(from: urlsAndOptions) { results in + downloadMultipleImages(with: urlsAndOptions) { results in let imagesMapper = ImageResultsMapper(results: results) let images = imagesMapper.mapErrors(with: placeholders) completion(images) diff --git a/Sources/StreamChatUI/Utils/ImageLoading/NukeImageLoader.swift b/Sources/StreamChatUI/Utils/ImageLoading/NukeImageLoader.swift index 32db4296bc0..83d0bac6f03 100644 --- a/Sources/StreamChatUI/Utils/ImageLoading/NukeImageLoader.swift +++ b/Sources/StreamChatUI/Utils/ImageLoading/NukeImageLoader.swift @@ -67,10 +67,11 @@ open class NukeImageLoader: ImageLoading { @discardableResult open func downloadImage( - from url: URL, - with options: ImageDownloadOptions = ImageDownloadOptions(), + with request: ImageDownloadRequest, completion: @escaping ((Result) -> Void) ) -> Cancellable? { + let url = request.url + let options = request.options let urlRequest = imageCDN.urlRequest(forImageUrl: url, resize: options.resize) let cachingKey = imageCDN.cachingKey(forImageUrl: url) @@ -99,16 +100,20 @@ open class NukeImageLoader: ImageLoading { } open func downloadMultipleImages( - from urlsAndOptions: [(url: URL, options: ImageDownloadOptions)], + with requests: [ImageDownloadRequest], completion: @escaping (([Result]) -> Void) ) { let group = DispatchGroup() var results: [Result] = [] - for (url, downloadOptions) in urlsAndOptions { + for request in requests { + let url = request.url + let downloadOptions = request.options + group.enter() - downloadImage(from: url, with: downloadOptions) { result in + let request = ImageDownloadRequest(url: url, options: downloadOptions) + downloadImage(with: request) { result in results.append(result) group.leave() diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index 0f9ff2cefc2..8339e482fbe 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -1069,6 +1069,8 @@ AD87D0A1263C7823008B466C /* AttachmentButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD87D0A0263C7823008B466C /* AttachmentButton.swift */; }; AD87D0AB263C7A7E008B466C /* ShrinkInputButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD87D0AA263C7A7E008B466C /* ShrinkInputButton.swift */; }; AD87D0BD263C7C09008B466C /* CircularCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD87D0BC263C7C09008B466C /* CircularCloseButton.swift */; }; + AD8B72752908016400921C31 /* ImageDownloadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8B72742908016400921C31 /* ImageDownloadRequest.swift */; }; + AD8B72762908016400921C31 /* ImageDownloadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8B72742908016400921C31 /* ImageDownloadRequest.swift */; }; AD8D1809268F7290004E3A5C /* TypingSuggester.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8D1808268F7290004E3A5C /* TypingSuggester.swift */; }; AD8D180B268F8ED4004E3A5C /* SlackComposerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8D180A268F8ED4004E3A5C /* SlackComposerVC.swift */; }; AD90D18525D56196001D03BB /* CurrentUserUpdater_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD90D18425D56196001D03BB /* CurrentUserUpdater_Tests.swift */; }; @@ -3281,6 +3283,7 @@ AD87D0A0263C7823008B466C /* AttachmentButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentButton.swift; sourceTree = ""; }; AD87D0AA263C7A7E008B466C /* ShrinkInputButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShrinkInputButton.swift; sourceTree = ""; }; AD87D0BC263C7C09008B466C /* CircularCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularCloseButton.swift; sourceTree = ""; }; + AD8B72742908016400921C31 /* ImageDownloadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloadRequest.swift; sourceTree = ""; }; AD8D1808268F7290004E3A5C /* TypingSuggester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingSuggester.swift; sourceTree = ""; }; AD8D180A268F8ED4004E3A5C /* SlackComposerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlackComposerVC.swift; sourceTree = ""; }; AD90D18425D56196001D03BB /* CurrentUserUpdater_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrentUserUpdater_Tests.swift; sourceTree = ""; }; @@ -6751,13 +6754,13 @@ ACCA772826C40C7A007AE2ED /* ImageLoading */ = { isa = PBXGroup; children = ( + AD8B7277290801B800921C31 /* ImageCDN */, ACCA772926C40C96007AE2ED /* ImageLoading.swift */, ACCA772B26C40D43007AE2ED /* NukeImageLoader.swift */, AD552E0028F46CE700199A6F /* ImageLoaderOptions.swift */, AD95FD1028FA038900DBDF41 /* ImageDownloadOptions.swift */, + AD8B72742908016400921C31 /* ImageDownloadRequest.swift */, AD95FD0C28F991ED00DBDF41 /* ImageResize.swift */, - BDC80CB4265CF4B800F62CE2 /* ImageCDN.swift */, - ADD2A99928FF4F4B00A83305 /* StreamCDN.swift */, ACD502A826BC0C670029FB7D /* ImageMerger.swift */, ADD2A98F28FF0CD300A83305 /* ImageSizeCalculator.swift */, AD7BBFCA2901AF3F004E8B76 /* ImageResultsMapper.swift */, @@ -6959,6 +6962,15 @@ path = ShrinkInputButton; sourceTree = ""; }; + AD8B7277290801B800921C31 /* ImageCDN */ = { + isa = PBXGroup; + children = ( + ADD2A99928FF4F4B00A83305 /* StreamCDN.swift */, + BDC80CB4265CF4B800F62CE2 /* ImageCDN.swift */, + ); + path = ImageCDN; + sourceTree = ""; + }; AD8E6BBD2642DB520013E01E /* CommandLabelView */ = { isa = PBXGroup; children = ( @@ -8687,6 +8699,7 @@ 88BD82B02549D18F00369074 /* ChatChannelListItemView.swift in Sources */, A3C5022B284F9CF70048753E /* Token.swift in Sources */, A39A8AE7263825F4003453D9 /* ChatMessageLayoutOptionsResolver.swift in Sources */, + AD8B72752908016400921C31 /* ImageDownloadRequest.swift in Sources */, 8825334C258CE82500B77352 /* AlertsRouter.swift in Sources */, AD95FD0D28F991ED00DBDF41 /* ImageResize.swift in Sources */, AD4474FD263B19F90030E583 /* ImageAttachmentComposerPreview.swift in Sources */, @@ -10246,6 +10259,7 @@ C121EBA32746A1E800023E4C /* QuotedChatMessageView.swift in Sources */, C121EBA42746A1E800023E4C /* QuotedChatMessageView+SwiftUI.swift in Sources */, C121EBA52746A1E800023E4C /* OnlineIndicatorView.swift in Sources */, + AD8B72762908016400921C31 /* ImageDownloadRequest.swift in Sources */, AD78F9FE28EC735700BC0FCE /* Token.swift in Sources */, C121EBA62746A1E800023E4C /* ChatPresenceAvatarView.swift in Sources */, C121EBA72746A1E800023E4C /* ChatAvatarView.swift in Sources */, diff --git a/Tests/StreamChatUITests/Mocks/ImageLoader_Mock.swift b/Tests/StreamChatUITests/Mocks/ImageLoader_Mock.swift index cd3963c884a..060dadcd384 100644 --- a/Tests/StreamChatUITests/Mocks/ImageLoader_Mock.swift +++ b/Tests/StreamChatUITests/Mocks/ImageLoader_Mock.swift @@ -7,12 +7,6 @@ import UIKit /// A mock implementation of the image loader which loads images synchronusly final class ImageLoader_Mock: ImageLoading { - func downloadImage(from url: URL, with options: ImageDownloadOptions, completion: @escaping ((Result) -> Void)) -> Cancellable? { - let image = UIImage(data: try! Data(contentsOf: url))! - completion(.success(image)) - return nil - } - func loadImage(into imageView: UIImageView, from url: URL?, with options: ImageLoaderOptions, completion: ((Result) -> Void)?) -> Cancellable? { if let url = url { let image = UIImage(data: try! Data(contentsOf: url))! @@ -24,12 +18,21 @@ final class ImageLoader_Mock: ImageLoading { return nil } + + func downloadImage( + with request: ImageDownloadRequest, + completion: @escaping ((Result) -> Void) + ) -> Cancellable? { + let image = UIImage(data: try! Data(contentsOf: request.url))! + completion(.success(image)) + return nil + } func downloadMultipleImages( - from urlsAndOptions: [(url: URL, options: ImageDownloadOptions)], + with requests: [ImageDownloadRequest], completion: @escaping (([Result]) -> Void) ) { - let results = urlsAndOptions.map(\.0).map { + let results = requests.map(\.url).map { Result.success(UIImage(data: try! Data(contentsOf: $0))!) } completion(results)