diff --git a/Caches/PrefetchRequestModifier.swift b/Caches/PrefetchRequestModifier.swift new file mode 100644 index 0000000..7a415d1 --- /dev/null +++ b/Caches/PrefetchRequestModifier.swift @@ -0,0 +1,15 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation +import Kingfisher + +struct PrefetchRequestModifier: ImageDownloadRequestModifier { + func modified(for request: URLRequest) -> URLRequest? { + var mutableRequest = request + + mutableRequest.allowsExpensiveNetworkAccess = false + mutableRequest.allowsConstrainedNetworkAccess = false + + return request + } +} diff --git a/Extensions/CollectionItem+Extensions.swift b/Extensions/CollectionItem+Extensions.swift index 3729e22..c4110a6 100644 --- a/Extensions/CollectionItem+Extensions.swift +++ b/Extensions/CollectionItem+Extensions.swift @@ -1,5 +1,6 @@ // Copyright © 2020 Metabolist. All rights reserved. +import Mastodon import UIKit import ViewModels @@ -61,4 +62,50 @@ extension CollectionItem { return UITableView.automaticDimension } } + + func mediaPrefetchURLs(identityContext: IdentityContext) -> Set { + switch self { + case let .status(status, _): + return status.mediaPrefetchURLs(identityContext: identityContext) + case let .account(account, _): + return account.mediaPrefetchURLs(identityContext: identityContext) + case let .notification(notification, _): + var urls = notification.account.mediaPrefetchURLs(identityContext: identityContext) + + if let status = notification.status { + urls.formUnion(status.mediaPrefetchURLs(identityContext: identityContext)) + } + + return urls + case let .conversation(conversation): + return conversation.accounts.reduce(Set()) { + $0.union($1.mediaPrefetchURLs(identityContext: identityContext)) + } + default: + return [] + } + } +} + +private extension Account { + func mediaPrefetchURLs(identityContext: IdentityContext) -> Set { + var urls = Set(emojis.map(\.url)) + + if !identityContext.appPreferences.shouldReduceMotion + && identityContext.appPreferences.animateAvatars == .everywhere { + urls.insert(avatar) + } else { + urls.insert(avatarStatic) + } + + return urls + } +} + +private extension Status { + func mediaPrefetchURLs(identityContext: IdentityContext) -> Set { + displayStatus.account.mediaPrefetchURLs(identityContext: identityContext) + .union(displayStatus.mediaAttachments.compactMap(\.previewUrl)) + .union(displayStatus.emojis.map(\.url)) + } } diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index aba8d92..612962f 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; }; D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */; }; D03D87F425C23C44004DCBB2 /* SecondaryNavigationTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03D87F325C23C44004DCBB2 /* SecondaryNavigationTitleView.swift */; }; + D0477F1525C68BAC005C5368 /* PrefetchRequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0477F1425C68BAC005C5368 /* PrefetchRequestModifier.swift */; }; D04F9E8E259E9C950081B0C9 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D04F9E8D259E9C950081B0C9 /* ViewModels */; }; D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */; }; D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */; }; @@ -251,6 +252,7 @@ D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentConfiguration.swift; sourceTree = ""; }; D036AA16254CA823009094DF /* StatusBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBodyView.swift; sourceTree = ""; }; D03D87F325C23C44004DCBB2 /* SecondaryNavigationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationTitleView.swift; sourceTree = ""; }; + D0477F1425C68BAC005C5368 /* PrefetchRequestModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefetchRequestModifier.swift; sourceTree = ""; }; D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; }; D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAttachmentViewController.swift; sourceTree = ""; }; D05936DD25A937EC00754FDF /* EditThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditThumbnailView.swift; sourceTree = ""; }; @@ -737,6 +739,7 @@ D025B14C25C4E482001C69A8 /* ImageCacheConfiguration.swift */, D025B14525C4D26A001C69A8 /* ImageCacheSerializer.swift */, D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */, + D0477F1425C68BAC005C5368 /* PrefetchRequestModifier.swift */, ); path = Caches; sourceTree = ""; @@ -967,6 +970,7 @@ D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */, D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */, D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */, + D0477F1525C68BAC005C5368 /* PrefetchRequestModifier.swift in Sources */, D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */, D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */, D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */, diff --git a/View Controllers/TableViewController.swift b/View Controllers/TableViewController.swift index 409dce2..f532f2d 100644 --- a/View Controllers/TableViewController.swift +++ b/View Controllers/TableViewController.swift @@ -2,6 +2,7 @@ import AVKit import Combine +import Kingfisher import Mastodon import SafariServices import SwiftUI @@ -48,6 +49,7 @@ class TableViewController: UITableViewController { super.viewDidLoad() tableView.dataSource = dataSource + tableView.prefetchDataSource = self tableView.cellLayoutMarginsFollowReadableWidth = true tableView.tableFooterView = UIView() tableView.contentInset.bottom = bottomInset @@ -189,6 +191,20 @@ extension TableViewController { } } +extension TableViewController: UITableViewDataSourcePrefetching { + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + let urls = indexPaths.compactMap(dataSource.itemIdentifier(for:)) + .reduce(Set()) { $0.union($1.mediaPrefetchURLs(identityContext: viewModel.identityContext)) } + var imageOptions = KingfisherManager.shared.defaultOptions + + imageOptions.append(.requestModifier(PrefetchRequestModifier())) + + for url in urls { + KingfisherManager.shared.retrieveImage(with: url, completionHandler: nil) + } + } +} + extension TableViewController: AVPlayerViewControllerDelegate { func playerViewController( _ playerViewController: AVPlayerViewController,