From f5e3ec7c764558965e034b96a9e42e46e8570c8b Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Jun 2021 19:58:28 +0800 Subject: [PATCH] feat: using Nuke framework for timeline image loading --- Mastodon.xcodeproj/project.pbxproj | 21 ++++- .../xcshareddata/swiftpm/Package.resolved | 13 ++- .../Diffiable/Section/StatusSection.swift | 48 ++++++++--- .../Container/MosaicImageViewContainer.swift | 81 +++++++++++++------ .../Scene/Share/View/Content/StatusView.swift | 10 +-- 5 files changed, 129 insertions(+), 44 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c168a219b..d874e2c8f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -416,6 +416,7 @@ 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 */; }; + DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; }; 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 */; }; @@ -781,7 +782,6 @@ 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 = ""; }; @@ -1050,6 +1050,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */, DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */, DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, @@ -1689,7 +1690,6 @@ children = ( DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */, DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */, - DB297B192679F5EF00704C90 /* ActiveLabel.swift */, DB3D0FED25BAA42200EAA174 /* MastodonSDK */, DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, @@ -2505,6 +2505,7 @@ DB9A487D2603456B008B817C /* UITextView+Placeholder */, DBB525072611EAC0002F1F29 /* Tabman */, DB6F5E31264E7410009108F4 /* TwitterTextEditor */, + DBAEDE5E267A0B1500D25FF5 /* Nuke */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -2694,6 +2695,7 @@ DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */, DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, + DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -4037,7 +4039,7 @@ repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { kind = exactVersion; - version = 5.0.2; + version = 5.0.3; }; }; 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { @@ -4112,6 +4114,14 @@ minimumVersion = 1.4.1; }; }; + DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kean/Nuke.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 10.3.0; + }; + }; DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/uias/Tabman"; @@ -4195,6 +4205,11 @@ package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; productName = "UITextView+Placeholder"; }; + DBAEDE5E267A0B1500D25FF5 /* Nuke */ = { + isa = XCSwiftPackageProductDependency; + package = DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */; + productName = Nuke; + }; DBB525072611EAC0002F1F29 /* Tabman */ = { isa = XCSwiftPackageProductDependency; package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */; diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 296feea28..500c37d34 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift", "state": { "branch": null, - "revision": "2132a7bf8da2bea74bb49b0b815950ea4d8f6a7e", - "version": "5.0.2" + "revision": "d503eb3bfabc54a70139618ab2ba09ebb8c09672", + "version": "5.0.3" } }, { @@ -73,6 +73,15 @@ "version": "6.2.1" } }, + { + "package": "Nuke", + "repositoryURL": "https://github.com/kean/Nuke.git", + "state": { + "branch": null, + "revision": "69ae6d5b8c4b898450432f94bd35f863d3830cfc", + "version": "10.3.0" + } + }, { "package": "Pageboy", "repositoryURL": "https://github.com/uias/Pageboy", diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 48b032152..5962e0a85 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -11,6 +11,7 @@ import CoreDataStack import os.log import UIKit import AVKit +import Nuke protocol StatusCell: DisposeBagCollectable { var statusView: StatusView { get } @@ -258,12 +259,13 @@ extension StatusSection { let mosaic = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) return [mosaic] } else { - let mosaics = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosaicImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) + let mosaics = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosaicImageViewModel.metas.count, maxSize: imageViewMaxSize) return mosaics } }() for (i, mosaic) in mosaics.enumerated() { - let (imageView, blurhashOverlayImageView) = mosaic + let imageView = mosaic.imageView + let blurhashOverlayImageView = mosaic.blurhashOverlayImageView let meta = mosaicImageViewModel.metas[i] meta.blurhashImagePublisher() @@ -272,18 +274,44 @@ extension StatusSection { blurhashOverlayImageView.image = image } .store(in: &cell.disposeBag) - imageView.af.setImage( - withURL: meta.url, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) { response in - switch response.result { - case .success: - statusItemAttribute.isImageLoaded.value = true + + let imageSize = CGSize( + width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale, + height: mosaic.imageViewSize.height * imageView.traitCollection.displayScale + ) + let request = ImageRequest( + url: meta.url, + processors: [ + ImageProcessors.Resize(size: imageSize, contentMode: .aspectFill) + ] + ) + let options = ImageLoadingOptions( + transition: .fadeIn(duration: 0.2) + ) + Nuke.loadImage( + with: request, + options: options, + into: imageView + ) { result in + switch result { case .failure: break + case .success: + statusItemAttribute.isImageLoaded.value = true } } + //imageView.af.setImage( + // withURL: meta.url, + // placeholderImage: UIImage.placeholder(color: .systemFill), + // imageTransition: .crossDissolve(0.2) + //) { response in + // switch response.result { + // case .success: + // statusItemAttribute.isImageLoaded.value = true + // case .failure: + // break + // } + //} imageView.accessibilityLabel = meta.altText Publishers.CombineLatest( statusItemAttribute.isImageLoaded, diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 081e99af4..68ed2a62c 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -107,7 +107,11 @@ extension MosaicImageViewContainer { container.spacing = 1 } - typealias ConfigurableMosaic = (imageView: UIImageView, blurhashOverlayImageView: UIImageView) + struct ConfigurableMosaic { + let imageView: UIImageView + let blurhashOverlayImageView: UIImageView + let imageViewSize: CGSize + } func setupImageView(aspectRatio: CGSize, maxSize: CGSize) -> ConfigurableMosaic { reset() @@ -163,15 +167,21 @@ extension MosaicImageViewContainer { contentWarningOverlayView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), ]) - return (imageView, blurhashOverlayImageView) + return ConfigurableMosaic( + imageView: imageView, + blurhashOverlayImageView: blurhashOverlayImageView, + imageViewSize: maxSize + ) } - func setupImageViews(count: Int, maxHeight: CGFloat) -> [ConfigurableMosaic] { + func setupImageViews(count: Int, maxSize: CGSize) -> [ConfigurableMosaic] { reset() guard count > 1 else { return [] } + let maxHeight = maxSize.height + containerHeightLayoutConstraint.constant = maxHeight containerHeightLayoutConstraint.isActive = true @@ -295,7 +305,35 @@ extension MosaicImageViewContainer { contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor), ]) - return zip(imageViews, blurhashOverlayImageViews).map { ($0, $1) } + var mosaics: [ConfigurableMosaic] = [] + for (i, (imageView, blurhashOverlayImageView)) in zip(imageViews, blurhashOverlayImageViews).enumerated() { + let imageViewSize: CGSize = { + switch (i, count) { + case (_, 4): + return CGSize(width: maxSize.width * 0.5, height: maxSize.height * 0.5) + case (i, 3): + let width = maxSize.width * 0.5 + if i == 0 { + return CGSize(width: width, height: maxSize.height) + } else { + return CGSize(width: width, height: maxSize.height * 0.5) + } + case (_, 2): + let width = maxSize.width * 0.5 + return CGSize(width: width, height: maxSize.height) + default: + assertionFailure() + return maxSize + } + }() + let mosaic = ConfigurableMosaic( + imageView: imageView, + blurhashOverlayImageView: blurhashOverlayImageView, + imageViewSize: imageViewSize + ) + mosaics.append(mosaic) + } + return mosaics } } @@ -366,11 +404,11 @@ struct MosaicImageView_Previews: PreviewProvider { UIViewPreview(width: 375) { let view = MosaicImageViewContainer() let image = images[3] - let (imageView, _) = view.setupImageView( + let mosaic = view.setupImageView( aspectRatio: image.size, maxSize: CGSize(width: 375, height: 400) ) - imageView.image = image + mosaic.imageView.image = image return view } .previewLayout(.fixed(width: 375, height: 400)) @@ -378,14 +416,14 @@ struct MosaicImageView_Previews: PreviewProvider { UIViewPreview(width: 375) { let view = MosaicImageViewContainer() let image = images[1] - let (imageView, _) = view.setupImageView( + let mosaic = view.setupImageView( aspectRatio: image.size, maxSize: CGSize(width: 375, height: 400) ) - imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = 8 - imageView.contentMode = .scaleAspectFill - imageView.image = image + mosaic.imageView.layer.masksToBounds = true + mosaic.imageView.layer.cornerRadius = 8 + mosaic.imageView.contentMode = .scaleAspectFill + mosaic.imageView.image = image return view } .previewLayout(.fixed(width: 375, height: 400)) @@ -393,10 +431,9 @@ struct MosaicImageView_Previews: PreviewProvider { UIViewPreview(width: 375) { let view = MosaicImageViewContainer() let images = self.images.prefix(2) - let mosaics = view.setupImageViews(count: images.count, maxHeight: 162) - for (i, mosiac) in mosaics.enumerated() { - let (imageView, _) = mosiac - imageView.image = images[i] + let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162)) + for (i, mosaic) in mosaics.enumerated() { + mosaic.imageView.image = images[i] } return view } @@ -405,10 +442,9 @@ struct MosaicImageView_Previews: PreviewProvider { UIViewPreview(width: 375) { let view = MosaicImageViewContainer() let images = self.images.prefix(3) - let mosaics = view.setupImageViews(count: images.count, maxHeight: 162) - for (i, mosiac) in mosaics.enumerated() { - let (imageView, _) = mosiac - imageView.image = images[i] + let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162)) + for (i, mosaic) in mosaics.enumerated() { + mosaic.imageView.image = images[i] } return view } @@ -417,10 +453,9 @@ struct MosaicImageView_Previews: PreviewProvider { UIViewPreview(width: 375) { let view = MosaicImageViewContainer() let images = self.images.prefix(4) - let mosaics = view.setupImageViews(count: images.count, maxHeight: 162) - for (i, mosiac) in mosaics.enumerated() { - let (imageView, _) = mosiac - imageView.image = images[i] + let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162)) + for (i, mosaic) in mosaics.enumerated() { + mosaic.imageView.image = images[i] } return view } diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 1aea1bcc3..337f07f04 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -619,10 +619,9 @@ struct StatusView_Previews: PreviewProvider { ) statusView.headerContainerView.isHidden = false let images = MosaicImageView_Previews.images - let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162) + let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162)) for (i, mosaic) in mosaics.enumerated() { - let (imageView, _) = mosaic - imageView.image = images[i] + mosaic.imageView.image = images[i] } statusView.statusMosaicImageViewContainer.isHidden = false statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = true @@ -644,10 +643,9 @@ struct StatusView_Previews: PreviewProvider { statusView.updateContentWarningDisplay(isHidden: false, animated: false) statusView.drawContentWarningImageView() let images = MosaicImageView_Previews.images - let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162) + let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162)) for (i, mosaic) in mosaics.enumerated() { - let (imageView, _) = mosaic - imageView.image = images[i] + mosaic.imageView.image = images[i] } statusView.statusMosaicImageViewContainer.isHidden = false return statusView