From a413adc613b129e0e5410764de7ef6b207c58f76 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Jun 2021 18:32:48 +0800 Subject: [PATCH] feat: cache placeholder and blurhash more efficiency --- Mastodon.xcodeproj/project.pbxproj | 10 +++ .../xcschemes/xcschememanagement.plist | 4 +- .../Diffiable/Section/StatusSection.swift | 61 ++++++-------- Mastodon/Extension/ActiveLabel.swift | 7 +- .../Protocol/AvatarConfigurableView.swift | 14 ++-- .../ViewModel/MosaicImageViewModel.swift | 32 +------- .../Service/BlurhashImageCacheService.swift | 82 +++++++++++++++++++ .../PlaceholderImageCacheService.swift | 60 ++++++++++++++ Mastodon/State/AppContext.swift | 3 + Mastodon/State/DocumentStore.swift | 1 - 10 files changed, 195 insertions(+), 79 deletions(-) create mode 100644 Mastodon/Service/BlurhashImageCacheService.swift create mode 100644 Mastodon/Service/PlaceholderImageCacheService.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index fa84dcee..c168a219 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -207,6 +207,7 @@ DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; }; DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; }; DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */; }; + DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */; }; DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; }; DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; }; DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; @@ -414,6 +415,7 @@ DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; }; DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; + DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; @@ -779,6 +781,8 @@ DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = ""; }; DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = ""; }; DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = ""; }; + DB297B192679F5EF00704C90 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ActiveLabel.swift; path = ../ActiveLabel.swift; sourceTree = ""; }; + DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderImageCacheService.swift; sourceTree = ""; }; DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = ""; }; DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; @@ -984,6 +988,7 @@ DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = ""; }; DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = ""; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = ""; }; + DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = ""; }; DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = ""; }; DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = ""; }; DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = ""; }; @@ -1350,6 +1355,8 @@ DB4924E126312AB200E9DB22 /* NotificationService.swift */, DB6D9F6226357848008423CD /* SettingService.swift */, DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */, + DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */, + DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */, ); path = Service; sourceTree = ""; @@ -1682,6 +1689,7 @@ children = ( DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */, DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */, + DB297B192679F5EF00704C90 /* ActiveLabel.swift */, DB3D0FED25BAA42200EAA174 /* MastodonSDK */, DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, @@ -2995,6 +3003,7 @@ DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */, DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */, DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */, + DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */, 2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */, 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */, 2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */, @@ -3100,6 +3109,7 @@ 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */, + DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */, 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */, DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 6d27be6a..e8494ef8 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -27,12 +27,12 @@ Mastodon.xcscheme_^#shared#^_ orderHint - 13 + 12 NotificationService.xcscheme_^#shared#^_ orderHint - 12 + 16 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index f5460b02..48b03215 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -84,7 +84,7 @@ extension StatusSection { case .root: StatusSection.configureThreadMeta(cell: cell, status: status) ManagedObjectObserver.observe(object: status.reblog ?? status) - .receive(on: DispatchQueue.main) + .receive(on: RunLoop.main) .sink { _ in // do nothing } receiveValue: { change in @@ -160,9 +160,9 @@ extension StatusSection { requestUserID: String, statusItemAttribute: Item.StatusAttribute ) { - // safely cancel the listenser when deleted + // safely cancel the listener when deleted ManagedObjectObserver.observe(object: status.reblog ?? status) - .receive(on: DispatchQueue.main) + .receive(on: RunLoop.main) .sink { _ in // do nothing } receiveValue: { [weak cell] change in @@ -174,11 +174,10 @@ extension StatusSection { } .store(in: &cell.disposeBag) - // set header StatusSection.configureHeader(cell: cell, status: status) ManagedObjectObserver.observe(object: status) - .receive(on: DispatchQueue.main) + .receive(on: RunLoop.main) .sink { _ in // do nothing } receiveValue: { [weak cell] change in @@ -221,7 +220,7 @@ extension StatusSection { cell.statusView.updateVisibility(visibility: visibility) cell.statusView.revealContentWarningButton.publisher(for: \.isHidden) - .receive(on: DispatchQueue.main) + .receive(on: RunLoop.main) .sink { [weak cell] isHidden in cell?.statusView.visibilityImageView.isHidden = !isHidden } @@ -234,7 +233,7 @@ extension StatusSection { let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } // set image - let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments) + let mosaicImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments) let imageViewMaxSize: CGSize = { let maxWidth: CGFloat = { // use timelinePostView width as container width @@ -246,43 +245,33 @@ extension StatusSection { return containerWidth }() let scale: CGFloat = { - switch mosiacImageViewModel.metas.count { + switch mosaicImageViewModel.metas.count { case 1: return 1.3 default: return 0.7 } }() return CGSize(width: maxWidth, height: maxWidth * scale) }() - let blurhashImageCache = dependency.context.documentStore.blurhashImageCache let mosaics: [MosaicImageViewContainer.ConfigurableMosaic] = { - if mosiacImageViewModel.metas.count == 1 { - let meta = mosiacImageViewModel.metas[0] + if mosaicImageViewModel.metas.count == 1 { + let meta = mosaicImageViewModel.metas[0] let mosaic = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) return [mosaic] } else { - let mosaics = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) + let mosaics = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosaicImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) return mosaics } }() - for (i, mosiac) in mosaics.enumerated() { - let (imageView, blurhashOverlayImageView) = mosiac - let meta = mosiacImageViewModel.metas[i] - let blurhashImageDataKey = meta.url.absoluteString as NSString - if let blurhashImageData = blurhashImageCache.object(forKey: meta.url.absoluteString as NSString), - let image = UIImage(data: blurhashImageData as Data) { - blurhashOverlayImageView.image = image - } else { - meta.blurhashImagePublisher() - .receive(on: DispatchQueue.main) - .sink { [weak blurhashImageCache] image in - guard let blurhashImageCache = blurhashImageCache else { return } - blurhashOverlayImageView.image = image - image?.pngData().flatMap { - blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey) - } - } - .store(in: &cell.disposeBag) - } + for (i, mosaic) in mosaics.enumerated() { + let (imageView, blurhashOverlayImageView) = mosaic + let meta = mosaicImageViewModel.metas[i] + + meta.blurhashImagePublisher() + .receive(on: RunLoop.main) + .sink { image in + blurhashOverlayImageView.image = image + } + .store(in: &cell.disposeBag) imageView.af.setImage( withURL: meta.url, placeholderImage: UIImage.placeholder(color: .systemFill), @@ -300,7 +289,7 @@ extension StatusSection { statusItemAttribute.isImageLoaded, statusItemAttribute.isRevealing ) - .receive(on: DispatchQueue.main) + .receive(on: RunLoop.main) .sink { [weak cell] isImageLoaded, isMediaRevealing in guard let cell = cell else { return } guard isImageLoaded else { @@ -322,7 +311,7 @@ extension StatusSection { } .store(in: &cell.disposeBag) } - cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty + cell.statusView.statusMosaicImageViewContainer.isHidden = mosaicImageViewModel.metas.isEmpty // set audio if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first { @@ -408,7 +397,7 @@ extension StatusSection { ) // observe model change ManagedObjectObserver.observe(object: status) - .receive(on: DispatchQueue.main) + .receive(on: RunLoop.main) .sink { _ in // do nothing } receiveValue: { [weak dependency, weak cell] change in @@ -479,7 +468,7 @@ extension StatusSection { // observe model change ManagedObjectObserver.observe(object: status.reblog ?? status) - .receive(on: DispatchQueue.main) + .receive(on: RunLoop.main) .sink { _ in // do nothing } receiveValue: { [weak dependency, weak cell] change in @@ -700,7 +689,7 @@ extension StatusSection { ManagedObjectObserver.observe(object: status.authorForUserProvider) .assertNoFailure() ) - .receive(on: DispatchQueue.main) + .receive(on: RunLoop.main) .sink { [weak dependency, weak cell] _, change in guard let cell = cell else { return } guard let dependency = dependency else { return } diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index 2c32bc4f..5d2ca7c6 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -63,6 +63,7 @@ extension ActiveLabel { extension ActiveLabel { /// status content func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) { + attributedText = nil activeEntities.removeAll() if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) { @@ -144,10 +145,10 @@ extension ActiveLabel { element.accessibilityLanguage = accessibilityLanguage elements.append(element) - for eneity in activeEntities { - guard let element = eneity.accessibilityElement(in: self) else { continue } + for entity in activeEntities { + guard let element = entity.accessibilityElement(in: self) else { continue } var glyphRange = NSRange() - layoutManager.characterRange(forGlyphRange: eneity.range, actualGlyphRange: &glyphRange) + layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange) let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) element.accessibilityFrame = self.convert(rect, to: nil) element.accessibilityContainer = self diff --git a/Mastodon/Protocol/AvatarConfigurableView.swift b/Mastodon/Protocol/AvatarConfigurableView.swift index 40ef9115..74bbd039 100644 --- a/Mastodon/Protocol/AvatarConfigurableView.swift +++ b/Mastodon/Protocol/AvatarConfigurableView.swift @@ -22,14 +22,14 @@ extension AvatarConfigurableView { public func configure(with configuration: AvatarConfigurableViewConfiguration) { let placeholderImage: UIImage = { - let placeholderImage = configuration.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageSize, color: .systemFill) - if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 { - return placeholderImage - .af.imageAspectScaled(toFill: Self.configurableAvatarImageSize) - .af.imageRounded(withCornerRadius: Self.configurableAvatarImageCornerRadius, divideRadiusByImageScale: false) - } else { - return placeholderImage.af.imageRoundedIntoCircle() + guard let placeholderImage = configuration.placeholderImage else { + return AppContext.shared.placeholderImageCacheService.image( + color: .systemFill, + size: Self.configurableAvatarImageSize, + cornerRadius: Self.configurableAvatarImageCornerRadius + ) } + return placeholderImage }() // cancel previous task diff --git a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift index 7d3dd275..888d4dff 100644 --- a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift @@ -45,40 +45,12 @@ struct MosaicMeta { let size: CGSize let blurhash: String? let altText: String? - - let workingQueue = DispatchQueue(label: "org.joinmastodon.app.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent) func blurhashImagePublisher() -> AnyPublisher { - return Future { promise in - workingQueue.async { - let image = self.blurhashImage() - promise(.success(image)) - } - } - .eraseToAnyPublisher() - } - - func blurhashImage() -> UIImage? { guard let blurhash = blurhash else { - return nil + return Just(nil).eraseToAnyPublisher() } - - let imageSize: CGSize = { - let aspectRadio = size.width / size.height - if size.width > size.height { - let width: CGFloat = MosaicMeta.edgeMaxLength - let height = width / aspectRadio - return CGSize(width: width, height: height) - } else { - let height: CGFloat = MosaicMeta.edgeMaxLength - let width = height * aspectRadio - return CGSize(width: width, height: height) - } - }() - - let image = UIImage(blurHash: blurhash, size: imageSize) - - return image + return AppContext.shared.blurhashImageCacheService.image(blurhash: blurhash, size: size, url: url) } } diff --git a/Mastodon/Service/BlurhashImageCacheService.swift b/Mastodon/Service/BlurhashImageCacheService.swift new file mode 100644 index 00000000..54526dcb --- /dev/null +++ b/Mastodon/Service/BlurhashImageCacheService.swift @@ -0,0 +1,82 @@ +// +// BlurhashImageCacheService.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-16. +// + +import UIKit +import Combine + +final class BlurhashImageCacheService { + + let cache = NSCache() + + let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent) + + func image(blurhash: String, size: CGSize, url: URL) -> AnyPublisher { + return Future { promise in + self.workingQueue.async { + let key = Key(blurhash: blurhash, size: size, url: url) + guard let image = self.cache.object(forKey: key) else { + if let image = BlurhashImageCacheService.blurhashImage(blurhash: blurhash, size: size, url: url) { + self.cache.setObject(image, forKey: key) + promise(.success(image)) + } else { + promise(.success(nil)) + } + return + } + promise(.success(image)) + } + } + .eraseToAnyPublisher() + } + + static func blurhashImage(blurhash: String, size: CGSize, url: URL) -> UIImage? { + let imageSize: CGSize = { + let aspectRadio = size.width / size.height + if size.width > size.height { + let width: CGFloat = MosaicMeta.edgeMaxLength + let height = width / aspectRadio + return CGSize(width: width, height: height) + } else { + let height: CGFloat = MosaicMeta.edgeMaxLength + let width = height * aspectRadio + return CGSize(width: width, height: height) + } + }() + + let image = UIImage(blurHash: blurhash, size: imageSize) + + return image + } + +} + +extension BlurhashImageCacheService { + class Key: Hashable { + static func == (lhs: BlurhashImageCacheService.Key, rhs: BlurhashImageCacheService.Key) -> Bool { + return lhs.blurhash == rhs.blurhash + && lhs.size == rhs.size + && lhs.url == rhs.url + } + + let blurhash: String + let size: CGSize + let url: URL + + init(blurhash: String, size: CGSize, url: URL) { + self.blurhash = blurhash + self.size = size + self.url = url + } + + func hash(into hasher: inout Hasher) { + hasher.combine(blurhash) + hasher.combine(size.width) + hasher.combine(size.height) + hasher.combine(url) + } + } +} diff --git a/Mastodon/Service/PlaceholderImageCacheService.swift b/Mastodon/Service/PlaceholderImageCacheService.swift new file mode 100644 index 00000000..ca56b331 --- /dev/null +++ b/Mastodon/Service/PlaceholderImageCacheService.swift @@ -0,0 +1,60 @@ +// +// PlaceholderImageCacheService.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-16. +// + +import UIKit +import AlamofireImage + +final class PlaceholderImageCacheService { + + let cache = NSCache() + + func image(color: UIColor, size: CGSize, cornerRadius: CGFloat = 0) -> UIImage { + let key = Key(color: color, size: size, cornerRadius: cornerRadius) + guard let image = cache.object(forKey: key) else { + var image = UIImage.placeholder(size: size, color: color) + if cornerRadius < size.width * 0.5 { + image = image + .af.imageAspectScaled(toFill: size) + .af.imageRounded(withCornerRadius: cornerRadius, divideRadiusByImageScale: false) + } else { + image = image.af.imageRoundedIntoCircle() + } + cache.setObject(image, forKey: key) + return image + } + + return image + } + +} + +extension PlaceholderImageCacheService { + class Key: Hashable { + let color: UIColor + let size: CGSize + let cornerRadius: CGFloat + + init(color: UIColor, size: CGSize, cornerRadius: CGFloat) { + self.color = color + self.size = size + self.cornerRadius = cornerRadius + } + + static func == (lhs: PlaceholderImageCacheService.Key, rhs: PlaceholderImageCacheService.Key) -> Bool { + return lhs.color == rhs.color + && lhs.size == rhs.size + && lhs.cornerRadius == rhs.cornerRadius + } + + func hash(into hasher: inout Hasher) { + hasher.combine(color) + hasher.combine(size.width) + hasher.combine(size.height) + hasher.combine(cornerRadius) + } + } +} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 89771dfb..e9b6073a 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -35,6 +35,9 @@ class AppContext: ObservableObject { let blockDomainService: BlockDomainService let photoLibraryService = PhotoLibraryService() + + let placeholderImageCacheService = PlaceholderImageCacheService() + let blurhashImageCacheService = BlurhashImageCacheService() let documentStore: DocumentStore private var documentStoreSubscription: AnyCancellable! diff --git a/Mastodon/State/DocumentStore.swift b/Mastodon/State/DocumentStore.swift index 8b3f88eb..cda03817 100644 --- a/Mastodon/State/DocumentStore.swift +++ b/Mastodon/State/DocumentStore.swift @@ -10,7 +10,6 @@ import Combine import MastodonSDK class DocumentStore: ObservableObject { - let blurhashImageCache = NSCache() let appStartUpTimestamp = Date() var defaultRevealStatusDict: [Mastodon.Entity.Status.ID: Bool] = [:] }