fix: GIF not load in some scene issue

This commit is contained in:
CMK 2021-07-02 19:48:56 +08:00
parent a614e0c156
commit 86da7a8ba1
22 changed files with 175 additions and 183 deletions

View File

@ -273,7 +273,6 @@
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; };
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.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 */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; };
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; };
@ -1146,7 +1145,6 @@
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, DBB525082611EAC0002F1F29 /* Tabman in Frameworks */,
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */, DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */,
@ -2677,7 +2675,6 @@
2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */,
2D42FF6025C8177C004A627A /* ActiveLabel */, 2D42FF6025C8177C004A627A /* ActiveLabel */,
DB0140BC25C40D7500F9F3CF /* CommonOSLog */, DB0140BC25C40D7500F9F3CF /* CommonOSLog */,
DB5086B725CC0D6400C2C187 /* Kingfisher */,
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
2D939AC725EE14620076FA61 /* CropViewController */, 2D939AC725EE14620076FA61 /* CropViewController */,
DB9A487D2603456B008B817C /* UITextView+Placeholder */, DB9A487D2603456B008B817C /* UITextView+Placeholder */,
@ -2871,7 +2868,6 @@
2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */,
2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */,
DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */,
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */,
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
@ -4801,14 +4797,6 @@
minimumVersion = 4.1.0; 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" */ = { DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git";
@ -4938,11 +4926,6 @@
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
productName = AlamofireImage; productName = AlamofireImage;
}; };
DB5086B725CC0D6400C2C187 /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
DB68050F2637D0F800430867 /* KeychainAccess */ = { DB68050F2637D0F800430867 /* KeychainAccess */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */;

View File

@ -12,7 +12,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>21</integer> <integer>20</integer>
</dict> </dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key> <key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -37,7 +37,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>20</integer> <integer>21</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -114,8 +114,8 @@
"repositoryURL": "https://github.com/TwidereProject/MetaTextView.git", "repositoryURL": "https://github.com/TwidereProject/MetaTextView.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "5b86b386464be8a6da5383aa714c458c07da6c01", "revision": "28e53130d16f12e0eeb479d83b77a0a718ef2088",
"version": "1.2.3" "version": "1.2.4"
} }
}, },
{ {

View File

@ -11,6 +11,7 @@ import CoreDataStack
import Foundation import Foundation
import MastodonSDK import MastodonSDK
import UIKit import UIKit
import Nuke
enum NotificationSection: Equatable, Hashable { enum NotificationSection: Equatable, Hashable {
case main case main
@ -72,10 +73,13 @@ extension NotificationSection {
} }
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
if let url = notification.account.avatarImageURL() { if let url = notification.account.avatarImageURL() {
cell.avatarImageView.af.setImage( cell.avatarImageViewTask = Nuke.loadImage(
withURL: url, with: url,
placeholderImage: UIImage.placeholder(color: .systemFill), options: ImageLoadingOptions(
imageTransition: .crossDissolve(0.2) placeholder: UIImage.placeholder(color: .systemFill),
transition: .fadeIn(duration: 0.2)
),
into: cell.avatarImageView
) )
} }
cell.avatarImageView.gesture().sink { [weak cell] _ in cell.avatarImageView.gesture().sink { [weak cell] _ in
@ -113,10 +117,13 @@ extension NotificationSection {
cell.actionLabel.text = actionText + " · " + timeText cell.actionLabel.text = actionText + " · " + timeText
cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict) cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict)
if let url = notification.account.avatarImageURL() { if let url = notification.account.avatarImageURL() {
cell.avatarImageView.af.setImage( cell.avatarImageViewTask = Nuke.loadImage(
withURL: url, with: url,
placeholderImage: UIImage.placeholder(color: .systemFill), options: ImageLoadingOptions(
imageTransition: .crossDissolve(0.2) placeholder: UIImage.placeholder(color: .systemFill),
transition: .fadeIn(duration: 0.2)
),
into: cell.avatarImageView
) )
} }
cell.avatarImageView.gesture().sink { [weak cell] _ in cell.avatarImageView.gesture().sink { [weak cell] _ in

View File

@ -547,7 +547,13 @@ extension StatusSection {
// name // name
let author = (status.reblog ?? status).author let author = (status.reblog ?? status).author
let nameContent = author.displayNameWithFallback 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 // username
cell.statusView.usernameLabel.text = "@" + author.acct cell.statusView.usernameLabel.text = "@" + author.acct
// avatar // avatar
@ -571,9 +577,10 @@ extension StatusSection {
) { ) {
// set content // set content
do { do {
let status = status.reblog ?? status
let content = MastodonContent( let content = MastodonContent(
content: (status.reblog ?? status).content, content: status.content,
emojis: (status.reblog ?? status).emojiMeta emojis: status.emojiMeta
) )
let metaContent = try MastodonMetaContent.convert(document: content) let metaContent = try MastodonMetaContent.convert(document: content)
cell.statusView.contentMetaText.configure(content: metaContent) cell.statusView.contentMetaText.configure(content: metaContent)
@ -648,46 +655,19 @@ extension StatusSection {
let isSingleMosaicLayout = mosaics.count == 1 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<LPLinkMetadata?, Error> { 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 // set image
let imageSize = CGSize( let imageSize = CGSize(
width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale, width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale,
height: mosaic.imageViewSize.height * 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( let request = ImageRequest(
url: meta.url, url: url,
processors: [ processors: [
ImageProcessors.Resize( ImageProcessors.Resize(
size: imageSize, size: imageSize,

View File

@ -57,6 +57,15 @@ extension ActiveLabel {
} }
extension ActiveLabel {
func configure(text: String) {
attributedText = nil
activeEntities.removeAll()
self.text = text
accessibilityLabel = text
}
}
extension ActiveLabel { extension ActiveLabel {
/// status content /// status content

View File

@ -15,7 +15,7 @@ enum MastodonStatusContent {
typealias EmojiShortcode = String typealias EmojiShortcode = String
typealias EmojiDict = [EmojiShortcode: URL] 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<MastodonStatusContent.ParseResult?, Never> { static func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> AnyPublisher<MastodonStatusContent.ParseResult?, Never> {
return Future { promise in return Future { promise in

View File

@ -14,27 +14,27 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
// prefetch reply status // prefetch reply status
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let domain = activeMastodonAuthenticationBox.domain let domain = activeMastodonAuthenticationBox.domain
let items = self.items(indexPaths: indexPaths)
var statusObjectIDs: [NSManagedObjectID] = []
for item in items(indexPaths: indexPaths) { let managedObjectContext = context.managedObjectContext
switch item { managedObjectContext.perform { [weak self] in
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
guard let self = self else { return } guard let self = self else { return }
for objectID in statusObjectIDs {
let status = backgroundManagedObjectContext.object(with: objectID) as! Status var statuses: [Status] = []
for item in items {
// fetch in-reply info if needs 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 { if let replyToID = status.inReplyToID, status.replyTo == nil {
self.context.statusPrefetchingService.prefetchReplyTo( self.context.statusPrefetchingService.prefetchReplyTo(
domain: domain, domain: domain,
@ -44,12 +44,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
authorizationBox: activeMastodonAuthenticationBox authorizationBox: activeMastodonAuthenticationBox
) )
} }
} // end for in
// self.context.statusContentCacheService.prefetch( } // end context.perform
// content: (status.reblog ?? status).content, } // end func
// emojiDict: (status.reblog ?? status).emojiDict
// )
}
}
}
} }

View File

@ -18,10 +18,6 @@ import MastodonSDK
import AlamofireImage import AlamofireImage
import AsyncDisplayKit import AsyncDisplayKit
#if DEBUG
import GDPerformanceView_Swift
#endif
final class AsyncHomeTimelineViewController: ASDKViewController<ASTableNode>, NeedsDependency, MediaPreviewableViewController { final class AsyncHomeTimelineViewController: ASDKViewController<ASTableNode>, NeedsDependency, MediaPreviewableViewController {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
@ -107,7 +103,6 @@ extension AsyncHomeTimelineViewController {
#if DEBUG #if DEBUG
// long press to trigger debug menu // long press to trigger debug menu
settingBarButtonItem.menu = debugMenu settingBarButtonItem.menu = debugMenu
PerformanceMonitor.shared().delegate = self
#else #else
settingBarButtonItem.target = self settingBarButtonItem.target = self
settingBarButtonItem.action = #selector(AsyncHomeTimelineViewController.settingBarButtonItemPressed(_:)) 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 // MARK: - ASTableDelegate
extension AsyncHomeTimelineViewController: ASTableDelegate { extension AsyncHomeTimelineViewController: ASTableDelegate {

View File

@ -77,7 +77,12 @@ final class HomeTimelineViewModel: NSObject {
let fetchRequest = HomeTimelineIndex.sortedFetchRequest let fetchRequest = HomeTimelineIndex.sortedFetchRequest
fetchRequest.fetchBatchSize = 20 fetchRequest.fetchBatchSize = 20
fetchRequest.returnsObjectsAsFaults = false 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( let controller = NSFetchedResultsController(
fetchRequest: fetchRequest, fetchRequest: fetchRequest,
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,

View File

@ -8,11 +8,12 @@
import os.log import os.log
import func AVFoundation.AVMakeRect import func AVFoundation.AVMakeRect
import UIKit import UIKit
import FLAnimatedImage
final class MediaPreviewImageView: UIScrollView { final class MediaPreviewImageView: UIScrollView {
let imageView: UIImageView = { let imageView: FLAnimatedImageView = {
let imageView = UIImageView() let imageView = FLAnimatedImageView()
imageView.contentMode = .scaleAspectFit imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true imageView.isUserInteractionEnabled = true
@ -120,7 +121,9 @@ extension MediaPreviewImageView {
} }
}() }()
imageView.frame = CGRect(origin: .zero, size: imageViewSize) imageView.frame = CGRect(origin: .zero, size: imageViewSize)
imageView.image = image if imageView.image == nil {
imageView.image = image
}
contentSize = imageViewSize contentSize = imageViewSize
contentInset = imageContentInset contentInset = imageContentInset

View File

@ -8,6 +8,7 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import Nuke
protocol MediaPreviewImageViewControllerDelegate: AnyObject { protocol MediaPreviewImageViewControllerDelegate: AnyObject {
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer) func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer)
@ -68,17 +69,36 @@ extension MediaPreviewImageViewController {
let previewImageViewContextMenuInteraction = UIContextMenuInteraction(delegate: self) let previewImageViewContextMenuInteraction = UIContextMenuInteraction(delegate: self)
previewImageView.addInteraction(previewImageViewContextMenuInteraction) previewImageView.addInteraction(previewImageViewContextMenuInteraction)
viewModel.image switch viewModel.item {
.receive(on: RunLoop.main) // use RunLoop prevent set image during zooming (TODO: handle transitioning state) case .local(let meta):
.sink { [weak self] image in self.previewImageView.imageView.image = meta.image
guard let self = self else { return } self.previewImageView.setup(image: meta.image, container: self.previewImageView, forceUpdate: true)
guard let image = image else { return } self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText
self.previewImageView.imageView.image = image case .status(let meta):
self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true) Nuke.loadImage(
self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText 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)
} }
} }

View File

@ -8,9 +8,11 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import AlamofireImage import Nuke
class MediaPreviewImageViewModel { class MediaPreviewImageViewModel {
var disposeBag = Set<AnyCancellable>()
// input // input
let item: ImagePreviewItem let item: ImagePreviewItem
@ -25,16 +27,20 @@ class MediaPreviewImageViewModel {
self.altText = meta.altText self.altText = meta.altText
let url = meta.url let url = meta.url
ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in
guard let self = self else { return } ImagePipeline.shared.imagePublisher(with: url)
switch response.result { .sink { completion in
case .failure(let error): switch completion {
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 .failure(let error):
case .success(let image): 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)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) case .finished:
self.image.value = image 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) { init(meta: LocalImagePreviewMeta) {

View File

@ -11,6 +11,8 @@ import UIKit
import ActiveLabel import ActiveLabel
import MetaTextView import MetaTextView
import Meta import Meta
import FLAnimatedImage
import Nuke
final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
static let actionImageBorderWidth: CGFloat = 2 static let actionImageBorderWidth: CGFloat = 2
@ -18,9 +20,10 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var pollCountdownSubscription: AnyCancellable? var pollCountdownSubscription: AnyCancellable?
var delegate: NotificationTableViewCellDelegate? var delegate: NotificationTableViewCellDelegate?
var avatarImageViewTask: ImageTask?
let avatarImageView: UIImageView = { let avatarImageView: UIImageView = {
let imageView = UIImageView() let imageView = FLAnimatedImageView()
imageView.layer.cornerRadius = 4 imageView.layer.cornerRadius = 4
imageView.layer.cornerCurve = .continuous imageView.layer.cornerCurve = .continuous
imageView.clipsToBounds = true imageView.clipsToBounds = true
@ -88,12 +91,12 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
avatarImageView.af.cancelImageRequest() avatarImageViewTask?.cancel()
avatarImageViewTask = nil
statusView.updateContentWarningDisplay(isHidden: true, animated: false) statusView.updateContentWarningDisplay(isHidden: true, animated: false)
statusView.pollTableView.dataSource = nil statusView.pollTableView.dataSource = nil
statusView.playerContainerView.reset() statusView.playerContainerView.reset()
statusView.playerContainerView.isHidden = true statusView.playerContainerView.isHidden = true
disposeBag.removeAll() disposeBag.removeAll()
} }

View File

@ -12,6 +12,8 @@ import UIKit
import Meta import Meta
import MetaTextView import MetaTextView
import ActiveLabel import ActiveLabel
import FLAnimatedImage
import Nuke
protocol NotificationTableViewCellDelegate: AnyObject { protocol NotificationTableViewCellDelegate: AnyObject {
var context: AppContext! { get } var context: AppContext! { get }
@ -37,9 +39,10 @@ final class NotificationTableViewCell: UITableViewCell {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var delegate: NotificationTableViewCellDelegate? var delegate: NotificationTableViewCellDelegate?
var avatarImageViewTask: ImageTask?
let avatarImageView: UIImageView = { let avatarImageView: UIImageView = {
let imageView = UIImageView() let imageView = FLAnimatedImageView()
imageView.layer.cornerRadius = 4 imageView.layer.cornerRadius = 4
imageView.layer.cornerCurve = .continuous imageView.layer.cornerCurve = .continuous
imageView.clipsToBounds = true imageView.clipsToBounds = true
@ -112,7 +115,8 @@ final class NotificationTableViewCell: UITableViewCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
avatarImageView.af.cancelImageRequest() avatarImageViewTask?.cancel()
avatarImageViewTask = nil
disposeBag.removeAll() disposeBag.removeAll()
} }

View File

@ -8,19 +8,27 @@
import func AVFoundation.AVMakeRect import func AVFoundation.AVMakeRect
import UIKit import UIKit
import Combine import Combine
import Nuke
import FLAnimatedImage
final class ContextMenuImagePreviewViewController: UIViewController { final class ContextMenuImagePreviewViewController: UIViewController {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var viewModel: ContextMenuImagePreviewViewModel! var viewModel: ContextMenuImagePreviewViewModel!
var imageTask: ImageTask?
let imageView: UIImageView = { let imageView: UIImageView = {
let imageView = UIImageView() let imageView = FLAnimatedImageView()
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
return imageView return imageView
}() }()
deinit {
imageTask?.cancel()
imageTask = nil
}
} }
@ -47,12 +55,13 @@ extension ContextMenuImagePreviewViewController {
.sink { [weak self] url in .sink { [weak self] url in
guard let self = self else { return } guard let self = self else { return }
guard let url = url else { return } guard let url = url else { return }
self.imageView.af.setImage( self.imageTask = Nuke.loadImage(
withURL: url, with: url,
placeholderImage: self.viewModel.thumbnail, options: ImageLoadingOptions(
imageTransition: .crossDissolve(0.2), placeholder: self.viewModel.thumbnail,
runImageTransitionIfCached: true, transition: .fadeIn(duration: 0.2)
completion: nil ),
into: self.imageView
) )
} }
.store(in: &disposeBag) .store(in: &disposeBag)

View File

@ -95,7 +95,12 @@ final class StatusView: UIView {
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
return view 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 avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
let nameLabel: ActiveLabel = { let nameLabel: ActiveLabel = {
@ -217,6 +222,7 @@ final class StatusView: UIView {
metaText.textView.textContainer.lineFragmentPadding = 0 metaText.textView.textContainer.lineFragmentPadding = 0
metaText.textView.textContainerInset = .zero metaText.textView.textContainerInset = .zero
metaText.textView.layer.masksToBounds = false metaText.textView.layer.masksToBounds = false
let paragraphStyle: NSMutableParagraphStyle = { let paragraphStyle: NSMutableParagraphStyle = {
let style = NSMutableParagraphStyle() let style = NSMutableParagraphStyle()
style.lineSpacing = 5 style.lineSpacing = 5
@ -234,7 +240,7 @@ final class StatusView: UIView {
] ]
return metaText return metaText
}() }()
private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
var isRevealing = true var isRevealing = true
@ -400,11 +406,6 @@ extension StatusView {
statusContainerStackView.addArrangedSubview(contentMetaText.textView) statusContainerStackView.addArrangedSubview(contentMetaText.textView)
contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical) contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
// TODO:
// link preview
// statusContainerStackView.addArrangedSubview(linkPreview)
// linkPreview.setContentHuggingPriority(.defaultHigh, for: .vertical)
// image // image
statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer) statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer)

View File

@ -25,7 +25,7 @@ final class AvatarStackContainerButton: UIControl {
static let maskOffset: CGFloat = 2 static let maskOffset: CGFloat = 2
// UIControl.Event - Application: 0x0F000000 // 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 var primaryActionState: UIControl.State = .normal
let topLeadingAvatarStackedImageView = AvatarStackedImageView() let topLeadingAvatarStackedImageView = AvatarStackedImageView()
@ -46,6 +46,12 @@ final class AvatarStackContainerButton: UIControl {
extension AvatarStackContainerButton { extension AvatarStackContainerButton {
private func _init() { 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 topLeadingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(topLeadingAvatarStackedImageView) addSubview(topLeadingAvatarStackedImageView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([

View File

@ -23,7 +23,7 @@ struct MosaicImageViewModel {
continue continue
} }
let mosaicMeta = MosaicMeta( let mosaicMeta = MosaicMeta(
priviewURL: element.previewURL.flatMap { URL(string: $0) }, previewURL: element.previewURL.flatMap { URL(string: $0) },
url: url, url: url,
size: CGSize(width: width, height: height), size: CGSize(width: width, height: height),
blurhash: element.blurhash, blurhash: element.blurhash,
@ -39,7 +39,7 @@ struct MosaicImageViewModel {
struct MosaicMeta { struct MosaicMeta {
static let edgeMaxLength: CGFloat = 20 static let edgeMaxLength: CGFloat = 20
let priviewURL: URL? let previewURL: URL?
let url: URL let url: URL
let size: CGSize let size: CGSize
let blurhash: String? let blurhash: String?

View File

@ -8,7 +8,6 @@
import os.log import os.log
import Foundation import Foundation
import GameplayKit import GameplayKit
import Kingfisher
import MastodonSDK import MastodonSDK
extension MastodonAttachmentService { extension MastodonAttachmentService {

View File

@ -9,7 +9,6 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import PhotosUI import PhotosUI
import Kingfisher
import GameplayKit import GameplayKit
import MobileCoreServices import MobileCoreServices
import MastodonSDK import MastodonSDK

View File

@ -11,7 +11,6 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import AlamofireImage import AlamofireImage
import Kingfisher
class AppContext: ObservableObject { class AppContext: ObservableObject {
@ -124,7 +123,6 @@ extension AppContext {
func purgeCache() -> AnyPublisher<ByteCount, Never> { func purgeCache() -> AnyPublisher<ByteCount, Never> {
Publishers.MergeMany([ Publishers.MergeMany([
AppContext.purgeAlamofireImageCache(), AppContext.purgeAlamofireImageCache(),
AppContext.purgeKingfisherCache(),
AppContext.purgeTemporaryDirectory(), AppContext.purgeTemporaryDirectory(),
]) ])
.reduce(0, +) .reduce(0, +)
@ -146,29 +144,6 @@ extension AppContext {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
private static func purgeKingfisherCache() -> AnyPublisher<ByteCount, Never> {
Future<ByteCount, Never> { 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<ByteCount, Never> { private static func purgeTemporaryDirectory() -> AnyPublisher<ByteCount, Never> {
Future<ByteCount, Never> { promise in Future<ByteCount, Never> { promise in
AppContext.purgeCacheWorkingQueue.async { AppContext.purgeCacheWorkingQueue.async {