feat: using Nuke framework for timeline image loading

This commit is contained in:
CMK 2021-06-16 19:58:28 +08:00
parent a413adc613
commit f5e3ec7c76
5 changed files with 129 additions and 44 deletions

View File

@ -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 = "<group>"; };
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = "<group>"; };
DB297B192679F5EF00704C90 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ActiveLabel.swift; path = ../ActiveLabel.swift; sourceTree = "<group>"; };
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderImageCacheService.swift; sourceTree = "<group>"; };
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
@ -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" */;

View File

@ -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",

View File

@ -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,

View File

@ -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
}

View File

@ -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