From 86da7a8ba1ae1913778af70252deb24f4b321510 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jul 2021 19:48:56 +0800 Subject: [PATCH] fix: GIF not load in some scene issue --- Mastodon.xcodeproj/project.pbxproj | 17 ------ .../xcschemes/xcschememanagement.plist | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Section/NotificationSection.swift | 23 +++++--- .../Diffiable/Section/StatusSection.swift | 54 ++++++------------- Mastodon/Extension/ActiveLabel.swift | 9 ++++ Mastodon/Helper/MastodonStatusContent.swift | 2 +- ...der+UITableViewDataSourcePrefetching.swift | 51 ++++++++---------- .../AsyncHomeTimelineViewController.swift | 12 ----- .../HomeTimeline/HomeTimelineViewModel.swift | 7 ++- .../Paging/Image/MediaPreviewImageView.swift | 9 ++-- .../MediaPreviewImageViewController.swift | 40 ++++++++++---- .../Image/MediaPreviewImageViewModel.swift | 26 +++++---- .../NotificationStatusTableViewCell.swift | 11 ++-- .../NotificationTableViewCell.swift | 10 ++-- ...ontextMenuImagePreviewViewController.swift | 25 ++++++--- .../Scene/Share/View/Content/StatusView.swift | 15 +++--- .../Control/AvatarStackContainerButton.swift | 8 ++- .../ViewModel/MosaicImageViewModel.swift | 4 +- ...astodonAttachmentService+UploadState.swift | 1 - .../MastodonAttachmentService.swift | 1 - Mastodon/State/AppContext.swift | 25 --------- 22 files changed, 175 insertions(+), 183 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3e8f8fdc..04e7da74 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -273,7 +273,6 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; @@ -1146,7 +1145,6 @@ DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */, @@ -2677,7 +2675,6 @@ 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, 2D42FF6025C8177C004A627A /* ActiveLabel */, DB0140BC25C40D7500F9F3CF /* CommonOSLog */, - DB5086B725CC0D6400C2C187 /* Kingfisher */, 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, 2D939AC725EE14620076FA61 /* CropViewController */, DB9A487D2603456B008B817C /* UITextView+Placeholder */, @@ -2871,7 +2868,6 @@ 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, @@ -4801,14 +4797,6 @@ minimumVersion = 4.1.0; }; }; - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/onevcat/Kingfisher.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 6.1.0; - }; - }; DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; @@ -4938,11 +4926,6 @@ package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; - DB5086B725CC0D6400C2C187 /* Kingfisher */ = { - isa = XCSwiftPackageProductDependency; - package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; - productName = Kingfisher; - }; DB68050F2637D0F800430867 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index da1d80e9..f1135b12 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 21 + 20 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -37,7 +37,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 20 + 21 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8414b940..817624a3 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -114,8 +114,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextView.git", "state": { "branch": null, - "revision": "5b86b386464be8a6da5383aa714c458c07da6c01", - "version": "1.2.3" + "revision": "28e53130d16f12e0eeb479d83b77a0a718ef2088", + "version": "1.2.4" } }, { diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 267f47b7..820f9d4b 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -11,6 +11,7 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit +import Nuke enum NotificationSection: Equatable, Hashable { case main @@ -72,10 +73,13 @@ extension NotificationSection { } .store(in: &cell.disposeBag) if let url = notification.account.avatarImageURL() { - cell.avatarImageView.af.setImage( - withURL: url, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) + cell.avatarImageViewTask = Nuke.loadImage( + with: url, + options: ImageLoadingOptions( + placeholder: UIImage.placeholder(color: .systemFill), + transition: .fadeIn(duration: 0.2) + ), + into: cell.avatarImageView ) } cell.avatarImageView.gesture().sink { [weak cell] _ in @@ -113,10 +117,13 @@ extension NotificationSection { cell.actionLabel.text = actionText + " ยท " + timeText cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict) if let url = notification.account.avatarImageURL() { - cell.avatarImageView.af.setImage( - withURL: url, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) + cell.avatarImageViewTask = Nuke.loadImage( + with: url, + options: ImageLoadingOptions( + placeholder: UIImage.placeholder(color: .systemFill), + transition: .fadeIn(duration: 0.2) + ), + into: cell.avatarImageView ) } cell.avatarImageView.gesture().sink { [weak cell] _ in diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index dd244eba..c5995124 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -547,7 +547,13 @@ extension StatusSection { // name let author = (status.reblog ?? status).author let nameContent = author.displayNameWithFallback - cell.statusView.nameLabel.configure(content: nameContent, emojiDict: author.emojiDict) + MastodonStatusContent.parseResult(content: nameContent, emojiDict: author.emojiDict) + .receive(on: DispatchQueue.main) + .sink { [weak cell] parseResult in + guard let cell = cell else { return } + cell.statusView.nameLabel.configure(contentParseResult: parseResult) + } + .store(in: &cell.disposeBag) // username cell.statusView.usernameLabel.text = "@" + author.acct // avatar @@ -571,9 +577,10 @@ extension StatusSection { ) { // set content do { + let status = status.reblog ?? status let content = MastodonContent( - content: (status.reblog ?? status).content, - emojis: (status.reblog ?? status).emojiMeta + content: status.content, + emojis: status.emojiMeta ) let metaContent = try MastodonMetaContent.convert(document: content) cell.statusView.contentMetaText.configure(content: metaContent) @@ -648,46 +655,19 @@ extension StatusSection { let isSingleMosaicLayout = mosaics.count == 1 - // set link preview -// cell.statusView.linkPreview.isHidden = true -// -// var _firstURL: URL? = { -// for entity in cell.statusView.activeTextLabel.activeEntities { -// guard case let .url(_, _, url, _) = entity.type else { continue } -// return URL(string: url) -// } -// return nil -// }() -// -// if let url = _firstURL { -// Future { promise in -// LPMetadataProvider().startFetchingMetadata(for: url) { meta, error in -// if let error = error { -// promise(.failure(error)) -// } else { -// promise(.success(meta)) -// } -// } -// } -// .receive(on: RunLoop.main) -// .sink { _ in -// // do nothing -// } receiveValue: { [weak cell] meta in -// guard let meta = meta else { return } -// guard let cell = cell else { return } -// cell.statusView.linkPreview.metadata = meta -// cell.statusView.linkPreview.isHidden = false -// } -// .store(in: &cell.disposeBag) -// } - // set image let imageSize = CGSize( width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale, height: mosaic.imageViewSize.height * imageView.traitCollection.displayScale ) + let url: URL? = { + if UIDevice.current.userInterfaceIdiom == .phone { + return meta.previewURL ?? meta.url + } + return meta.url + }() let request = ImageRequest( - url: meta.url, + url: url, processors: [ ImageProcessors.Resize( size: imageSize, diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index b4f89e71..ebb82655 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -57,6 +57,15 @@ extension ActiveLabel { } +extension ActiveLabel { + func configure(text: String) { + attributedText = nil + activeEntities.removeAll() + self.text = text + accessibilityLabel = text + } +} + extension ActiveLabel { /// status content diff --git a/Mastodon/Helper/MastodonStatusContent.swift b/Mastodon/Helper/MastodonStatusContent.swift index 1e52f150..d19463a8 100755 --- a/Mastodon/Helper/MastodonStatusContent.swift +++ b/Mastodon/Helper/MastodonStatusContent.swift @@ -15,7 +15,7 @@ enum MastodonStatusContent { typealias EmojiShortcode = String typealias EmojiDict = [EmojiShortcode: URL] - static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive) + static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive, attributes: .concurrent) static func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> AnyPublisher { return Future { promise in diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift index a9e1898a..5d6e81d5 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift @@ -14,27 +14,27 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { // prefetch reply status guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } let domain = activeMastodonAuthenticationBox.domain - - var statusObjectIDs: [NSManagedObjectID] = [] - for item in items(indexPaths: indexPaths) { - switch item { - case .homeTimelineIndex(let objectID, _): - let homeTimelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex - statusObjectIDs.append(homeTimelineIndex.status.objectID) - case .status(let objectID, _): - statusObjectIDs.append(objectID) - default: - continue - } - } - - let backgroundManagedObjectContext = context.backgroundManagedObjectContext - backgroundManagedObjectContext.perform { [weak self] in + let items = self.items(indexPaths: indexPaths) + + let managedObjectContext = context.managedObjectContext + managedObjectContext.perform { [weak self] in guard let self = self else { return } - for objectID in statusObjectIDs { - let status = backgroundManagedObjectContext.object(with: objectID) as! Status - - // fetch in-reply info if needs + + var statuses: [Status] = [] + for item in items { + switch item { + case .homeTimelineIndex(let objectID, _): + guard let homeTimelineIndex = try? managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { continue } + statuses.append(homeTimelineIndex.status) + case .status(let objectID, _): + guard let status = try? managedObjectContext.existingObject(with: objectID) as? Status else { continue } + statuses.append(status) + default: + continue + } + } + + for status in statuses { if let replyToID = status.inReplyToID, status.replyTo == nil { self.context.statusPrefetchingService.prefetchReplyTo( domain: domain, @@ -44,12 +44,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { authorizationBox: activeMastodonAuthenticationBox ) } - -// self.context.statusContentCacheService.prefetch( -// content: (status.reblog ?? status).content, -// emojiDict: (status.reblog ?? status).emojiDict -// ) - } - } - } + } // end for in + } // end context.perform + } // end func } diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift index 24736480..c8ccfdd3 100644 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift @@ -18,10 +18,6 @@ import MastodonSDK import AlamofireImage import AsyncDisplayKit -#if DEBUG -import GDPerformanceView_Swift -#endif - final class AsyncHomeTimelineViewController: ASDKViewController, NeedsDependency, MediaPreviewableViewController { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } @@ -107,7 +103,6 @@ extension AsyncHomeTimelineViewController { #if DEBUG // long press to trigger debug menu settingBarButtonItem.menu = debugMenu - PerformanceMonitor.shared().delegate = self #else settingBarButtonItem.target = self settingBarButtonItem.action = #selector(AsyncHomeTimelineViewController.settingBarButtonItemPressed(_:)) @@ -548,13 +543,6 @@ extension AsyncHomeTimelineViewController: StatusTableViewControllerNavigateable } } -#if DEBUG -extension AsyncHomeTimelineViewController: PerformanceMonitorDelegate { - func performanceMonitor(didReport performanceReport: PerformanceReport) { - // print(performanceReport) - } -} -#endif // MARK: - ASTableDelegate extension AsyncHomeTimelineViewController: ASTableDelegate { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index fdbbfba9..d22c52be 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -77,7 +77,12 @@ final class HomeTimelineViewModel: NSObject { let fetchRequest = HomeTimelineIndex.sortedFetchRequest fetchRequest.fetchBatchSize = 20 fetchRequest.returnsObjectsAsFaults = false - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(HomeTimelineIndex.status)] + fetchRequest.relationshipKeyPathsForPrefetching = [ + #keyPath(HomeTimelineIndex.status), + #keyPath(HomeTimelineIndex.status.author), + #keyPath(HomeTimelineIndex.status.reblog), + #keyPath(HomeTimelineIndex.status.reblog.author), + ] let controller = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: context.managedObjectContext, diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift index aa11e494..05f2ce70 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift @@ -8,11 +8,12 @@ import os.log import func AVFoundation.AVMakeRect import UIKit +import FLAnimatedImage final class MediaPreviewImageView: UIScrollView { - let imageView: UIImageView = { - let imageView = UIImageView() + let imageView: FLAnimatedImageView = { + let imageView = FLAnimatedImageView() imageView.contentMode = .scaleAspectFit imageView.clipsToBounds = true imageView.isUserInteractionEnabled = true @@ -120,7 +121,9 @@ extension MediaPreviewImageView { } }() imageView.frame = CGRect(origin: .zero, size: imageViewSize) - imageView.image = image + if imageView.image == nil { + imageView.image = image + } contentSize = imageViewSize contentInset = imageContentInset diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift index e1f2736f..5ec82db9 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import Nuke protocol MediaPreviewImageViewControllerDelegate: AnyObject { func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer) @@ -68,17 +69,36 @@ extension MediaPreviewImageViewController { let previewImageViewContextMenuInteraction = UIContextMenuInteraction(delegate: self) previewImageView.addInteraction(previewImageViewContextMenuInteraction) - - viewModel.image - .receive(on: RunLoop.main) // use RunLoop prevent set image during zooming (TODO: handle transitioning state) - .sink { [weak self] image in - guard let self = self else { return } - guard let image = image else { return } - self.previewImageView.imageView.image = image - self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true) - self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText + + switch viewModel.item { + case .local(let meta): + self.previewImageView.imageView.image = meta.image + self.previewImageView.setup(image: meta.image, container: self.previewImageView, forceUpdate: true) + self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText + case .status(let meta): + Nuke.loadImage( + with: meta.url, + into: self.previewImageView.imageView + ) { result in + switch result { + case .failure(let error): + break + case .success(let response): + self.previewImageView.setup(image: response.image, container: self.previewImageView, forceUpdate: true) + self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText + } } - .store(in: &disposeBag) + } +// viewModel.image +// .receive(on: RunLoop.main) // use RunLoop prevent set image during zooming (TODO: handle transitioning state) +// .sink { [weak self] image in +// guard let self = self else { return } +// guard let image = image else { return } +// self.previewImageView.imageView.image = image +// self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true) +// self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText +// } +// .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift index c9afac8c..a6163a8c 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift @@ -8,9 +8,11 @@ import os.log import UIKit import Combine -import AlamofireImage +import Nuke class MediaPreviewImageViewModel { + + var disposeBag = Set() // input let item: ImagePreviewItem @@ -25,16 +27,20 @@ class MediaPreviewImageViewModel { self.altText = meta.altText let url = meta.url - ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in - guard let self = self else { return } - switch response.result { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) - case .success(let image): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) - self.image.value = image + + ImagePipeline.shared.imagePublisher(with: url) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + self.image.value = response.image } - }) + .store(in: &disposeBag) } init(meta: LocalImagePreviewMeta) { diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index d2231d4f..83fa5009 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -11,6 +11,8 @@ import UIKit import ActiveLabel import MetaTextView import Meta +import FLAnimatedImage +import Nuke final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { static let actionImageBorderWidth: CGFloat = 2 @@ -18,9 +20,10 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { var disposeBag = Set() var pollCountdownSubscription: AnyCancellable? var delegate: NotificationTableViewCellDelegate? - + + var avatarImageViewTask: ImageTask? let avatarImageView: UIImageView = { - let imageView = UIImageView() + let imageView = FLAnimatedImageView() imageView.layer.cornerRadius = 4 imageView.layer.cornerCurve = .continuous imageView.clipsToBounds = true @@ -88,12 +91,12 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { override func prepareForReuse() { super.prepareForReuse() - avatarImageView.af.cancelImageRequest() + avatarImageViewTask?.cancel() + avatarImageViewTask = nil statusView.updateContentWarningDisplay(isHidden: true, animated: false) statusView.pollTableView.dataSource = nil statusView.playerContainerView.reset() statusView.playerContainerView.isHidden = true - disposeBag.removeAll() } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 15fa21c6..30b4ce1b 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -12,6 +12,8 @@ import UIKit import Meta import MetaTextView import ActiveLabel +import FLAnimatedImage +import Nuke protocol NotificationTableViewCellDelegate: AnyObject { var context: AppContext! { get } @@ -37,9 +39,10 @@ final class NotificationTableViewCell: UITableViewCell { var disposeBag = Set() var delegate: NotificationTableViewCellDelegate? - + + var avatarImageViewTask: ImageTask? let avatarImageView: UIImageView = { - let imageView = UIImageView() + let imageView = FLAnimatedImageView() imageView.layer.cornerRadius = 4 imageView.layer.cornerCurve = .continuous imageView.clipsToBounds = true @@ -112,7 +115,8 @@ final class NotificationTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - avatarImageView.af.cancelImageRequest() + avatarImageViewTask?.cancel() + avatarImageViewTask = nil disposeBag.removeAll() } diff --git a/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift b/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift index 2a5ba492..28b13e2f 100644 --- a/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift +++ b/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift @@ -8,19 +8,27 @@ import func AVFoundation.AVMakeRect import UIKit import Combine +import Nuke +import FLAnimatedImage final class ContextMenuImagePreviewViewController: UIViewController { var disposeBag = Set() var viewModel: ContextMenuImagePreviewViewModel! - + + var imageTask: ImageTask? let imageView: UIImageView = { - let imageView = UIImageView() + let imageView = FLAnimatedImageView() imageView.contentMode = .scaleAspectFill imageView.layer.masksToBounds = true return imageView }() + + deinit { + imageTask?.cancel() + imageTask = nil + } } @@ -47,12 +55,13 @@ extension ContextMenuImagePreviewViewController { .sink { [weak self] url in guard let self = self else { return } guard let url = url else { return } - self.imageView.af.setImage( - withURL: url, - placeholderImage: self.viewModel.thumbnail, - imageTransition: .crossDissolve(0.2), - runImageTransitionIfCached: true, - completion: nil + self.imageTask = Nuke.loadImage( + with: url, + options: ImageLoadingOptions( + placeholder: self.viewModel.thumbnail, + transition: .fadeIn(duration: 0.2) + ), + into: self.imageView ) } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 2b51f028..7ed828fa 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -95,7 +95,12 @@ final class StatusView: UIView { view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile return view }() - let avatarImageView: UIImageView = FLAnimatedImageView() + let avatarImageView: UIImageView = { + let imageView = FLAnimatedImageView() + imageView.layer.shouldRasterize = true + imageView.layer.rasterizationScale = UIScreen.main.scale + return imageView + }() let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton() let nameLabel: ActiveLabel = { @@ -217,6 +222,7 @@ final class StatusView: UIView { metaText.textView.textContainer.lineFragmentPadding = 0 metaText.textView.textContainerInset = .zero metaText.textView.layer.masksToBounds = false + let paragraphStyle: NSMutableParagraphStyle = { let style = NSMutableParagraphStyle() style.lineSpacing = 5 @@ -234,7 +240,7 @@ final class StatusView: UIView { ] return metaText }() - + private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer var isRevealing = true @@ -400,11 +406,6 @@ extension StatusView { statusContainerStackView.addArrangedSubview(contentMetaText.textView) contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical) - // TODO: - // link preview - // statusContainerStackView.addArrangedSubview(linkPreview) - // linkPreview.setContentHuggingPriority(.defaultHigh, for: .vertical) - // image statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer) diff --git a/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift b/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift index 965d710d..d16a952f 100644 --- a/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift +++ b/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift @@ -25,7 +25,7 @@ final class AvatarStackContainerButton: UIControl { static let maskOffset: CGFloat = 2 // UIControl.Event - Application: 0x0F000000 - static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000 + static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000 var primaryActionState: UIControl.State = .normal let topLeadingAvatarStackedImageView = AvatarStackedImageView() @@ -46,6 +46,12 @@ final class AvatarStackContainerButton: UIControl { extension AvatarStackContainerButton { private func _init() { + topLeadingAvatarStackedImageView.layer.shouldRasterize = true + topLeadingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale + + bottomTrailingAvatarStackedImageView.layer.shouldRasterize = true + bottomTrailingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale + topLeadingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false addSubview(topLeadingAvatarStackedImageView) NSLayoutConstraint.activate([ diff --git a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift index 265ce245..5ceb8781 100644 --- a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift @@ -23,7 +23,7 @@ struct MosaicImageViewModel { continue } let mosaicMeta = MosaicMeta( - priviewURL: element.previewURL.flatMap { URL(string: $0) }, + previewURL: element.previewURL.flatMap { URL(string: $0) }, url: url, size: CGSize(width: width, height: height), blurhash: element.blurhash, @@ -39,7 +39,7 @@ struct MosaicImageViewModel { struct MosaicMeta { static let edgeMaxLength: CGFloat = 20 - let priviewURL: URL? + let previewURL: URL? let url: URL let size: CGSize let blurhash: String? diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift index b154d17c..f773115d 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift @@ -8,7 +8,6 @@ import os.log import Foundation import GameplayKit -import Kingfisher import MastodonSDK extension MastodonAttachmentService { diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index ede5c64b..cafa3145 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine import PhotosUI -import Kingfisher import GameplayKit import MobileCoreServices import MastodonSDK diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index ef758951..f1de6dd7 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -11,7 +11,6 @@ import Combine import CoreData import CoreDataStack import AlamofireImage -import Kingfisher class AppContext: ObservableObject { @@ -124,7 +123,6 @@ extension AppContext { func purgeCache() -> AnyPublisher { Publishers.MergeMany([ AppContext.purgeAlamofireImageCache(), - AppContext.purgeKingfisherCache(), AppContext.purgeTemporaryDirectory(), ]) .reduce(0, +) @@ -146,29 +144,6 @@ extension AppContext { .eraseToAnyPublisher() } - private static func purgeKingfisherCache() -> AnyPublisher { - Future { promise in - KingfisherManager.shared.cache.calculateDiskStorageSize { result in - switch result { - case .success(let diskBytes): - KingfisherManager.shared.cache.clearCache() - KingfisherManager.shared.cache.calculateDiskStorageSize { currentResult in - switch currentResult { - case .success(let currentDiskBytes): - let purgedDiskBytes = max(0, Int(diskBytes) - Int(currentDiskBytes)) - promise(.success(purgedDiskBytes)) - case .failure: - promise(.success(0)) - } - } - case .failure: - promise(.success(0)) - } - } - } - .eraseToAnyPublisher() - } - private static func purgeTemporaryDirectory() -> AnyPublisher { Future { promise in AppContext.purgeCacheWorkingQueue.async {