Add LazyVStack to lists

This commit is contained in:
Marcin Czachursk 2023-01-26 20:35:24 +01:00
parent 50c26e9e4d
commit c652b34430
12 changed files with 150 additions and 89 deletions

View File

@ -27,6 +27,7 @@
F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE42966E160001D9973 /* Color+SystemColors.swift */; };
F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; };
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
F829193C2983012400367CE2 /* ImageSizeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F829193B2983012400367CE2 /* ImageSizeService.swift */; };
F8341F90295C636C009C8EE6 /* Data+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* Data+Exif.swift */; };
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; };
F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */; };
@ -134,6 +135,7 @@
F8210DE42966E160001D9973 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = "<group>"; };
F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = "<group>"; };
F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; };
F829193B2983012400367CE2 /* ImageSizeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSizeService.swift; sourceTree = "<group>"; };
F8341F8F295C636C009C8EE6 /* Data+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Exif.swift"; sourceTree = "<group>"; };
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; };
F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarouselPicture.swift; sourceTree = "<group>"; };
@ -456,6 +458,7 @@
F88E4D49297EA0490057491A /* RouterPath.swift */,
F88E4D59297ECEE60057491A /* SearchService.swift */,
F8C7EDBE298169EE002843BC /* TagsService.swift */,
F829193B2983012400367CE2 /* ImageSizeService.swift */,
);
path = Services;
sourceTree = "<group>";
@ -620,6 +623,7 @@
F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */,
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */,
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */,
F829193C2983012400367CE2 /* ImageSizeService.swift in Sources */,
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */,
F8210DE12966D0C4001D9973 /* StatusService.swift in Sources */,
F85DBF8F296732E20069BF89 /* AccountsView.swift in Sources */,

View File

@ -14,7 +14,7 @@ extension AttachmentData {
}
@NSManaged public var blurhash: String?
@NSManaged public var data: Data
@NSManaged public var data: Data?
@NSManaged public var exifCamera: String?
@NSManaged public var exifCreatedDate: String?
@NSManaged public var exifExposure: String?

View File

@ -11,16 +11,16 @@ public class CacheImageService {
public static let shared = CacheImageService()
private init() { }
private var memoryChartData = MemoryCache<URL, Image>(entryLifetime: 600)
private var memoryCacheData = MemoryCache<URL, Image>(entryLifetime: 600)
func addImage(for url: URL, data: Data) {
if let uiImage = UIImage(data: data) {
self.memoryChartData[url] = Image(uiImage: uiImage)
self.memoryCacheData[url] = Image(uiImage: uiImage)
}
}
func addImage(for url: URL, image: Image) {
self.memoryChartData[url] = image
self.memoryCacheData[url] = image
}
func downloadImage(url: URL?) async {
@ -28,7 +28,7 @@ public class CacheImageService {
return
}
if memoryChartData[url] != nil {
if memoryCacheData[url] != nil {
return
}
@ -43,6 +43,6 @@ public class CacheImageService {
}
func getImage(for url: URL) -> Image? {
return self.memoryChartData[url]
return self.memoryCacheData[url]
}
}

View File

@ -0,0 +1,45 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
import SwiftUI
public class ImageSizeService {
public static let shared = ImageSizeService()
private init() { }
private var memoryCacheData = MemoryCache<URL, CGSize>(entryLifetime: 3600)
func addImageSize(for url: URL, size: CGSize) {
self.memoryCacheData[url] = size
}
func getImageSize(for url: URL) -> CGSize? {
return self.memoryCacheData[url]
}
func calculateSize(for url: URL, width: Int32, height: Int32) -> CGSize {
return calculateSize(for: url, width: Double(width), height: Double(height))
}
func calculateSize(for url: URL, width: Int, height: Int) -> CGSize {
return calculateSize(for: url, width: Double(width), height: Double(height))
}
func calculateSize(for url: URL, width: Double, height: Double) -> CGSize {
let divider = Double(width) / UIScreen.main.bounds.size.width
let calculatedHeight = Double(height) / divider
let size = CGSize(
width: UIScreen.main.bounds.width,
height: (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
)
self.memoryCacheData.insert(size, forKey: url)
return size
}
}

View File

@ -13,7 +13,12 @@ public class RemoteFileService {
private init() { }
public func fetchData(url: URL) async throws -> Data? {
let (data, response) = try await ImagePipeline.shared.data(for: url)
let request = ImageRequest(
url: url,
priority: .high
)
let (data, response) = try await ImagePipeline.shared.data(for: request)
guard let response else {
return data

View File

@ -9,7 +9,6 @@ import MastodonKit
public class StatusViewModel: ObservableObject {
public let uniqueId: UUID
public let id: EntityId
public let content: Html
@ -69,7 +68,6 @@ public class StatusViewModel: ObservableObject {
tags: [Tag] = [],
place: Place? = nil
) {
self.uniqueId = UUID()
self.id = id
self.content = content
self.uri = uri
@ -103,7 +101,6 @@ public class StatusViewModel: ObservableObject {
// If status has been rebloged we are saving orginal status here.
let orginalStatus = status.reblog ?? status
self.uniqueId = UUID()
self.id = orginalStatus.id
self.content = orginalStatus.content
self.uri = orginalStatus.uri

View File

@ -30,9 +30,9 @@ struct StatusesView: View {
var body: some View {
ScrollView {
VStack(alignment: .center) {
if firstLoadFinished == true {
ForEach(self.statusViewModels, id: \.uniqueId) { item in
if firstLoadFinished == true {
LazyVStack(alignment: .center) {
ForEach(self.statusViewModels, id: \.id) { item in
NavigationLink(value: RouteurDestinations.status(
id: item.id,
blurhash: item.mediaAttachments.first?.blurhash,
@ -43,24 +43,21 @@ struct StatusesView: View {
}
.buttonStyle(EmptyButtonStyle())
}
LazyVStack {
if allItemsLoaded == false && firstLoadFinished == true {
HStack {
Spacer()
LoadingIndicator()
.task {
do {
try await self.loadMoreStatuses()
} catch {
ErrorService.shared.handle(error, message: "Loading more statuses failed.", showToastr: true)
}
if allItemsLoaded == false && firstLoadFinished == true {
HStack {
Spacer()
LoadingIndicator()
.task {
do {
try await self.loadMoreStatuses()
} catch {
ErrorService.shared.handle(error, message: "Loading more statuses failed.", showToastr: true)
}
Spacer()
}
}
Spacer()
}
}
}
}
}

View File

@ -40,9 +40,9 @@ struct TrendStatusesView: View {
}
}
VStack(alignment: .center) {
LazyVStack(alignment: .center) {
if firstLoadFinished == true {
ForEach(self.statusViewModels, id: \.uniqueId) { item in
ForEach(self.statusViewModels, id: \.id) { item in
NavigationLink(value: RouteurDestinations.status(
id: item.id,
blurhash: item.mediaAttachments.first?.blurhash,

View File

@ -18,26 +18,25 @@ struct ImageRow: View {
self.status = statusData
self.attachmentData = statusData.attachments().first
if let imageData = self.attachmentData?.data, let uiImage = UIImage(data: imageData) {
// Calculate size of frame (first from cache, then from real image, then from metadata).
if let attachmentData, let size = ImageSizeService.shared.getImageSize(for: attachmentData.url) {
self.imageWidth = size.width
self.imageHeight = size.height
} else if let attachmentData, let imageData = attachmentData.data, let uiImage = UIImage(data: imageData) {
self.uiImage = uiImage
let imgHeight = uiImage.size.height
let imgWidth = uiImage.size.width
let divider = imgWidth / UIScreen.main.bounds.size.width
let calculatedHeight = imgHeight / divider
self.imageWidth = UIScreen.main.bounds.width
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
} else if let imgWidth = attachmentData?.metaImageWidth, let imgHeight = attachmentData?.metaImageHeight {
let divider = Double(imgWidth) / UIScreen.main.bounds.size.width
let calculatedHeight = Double(imgHeight) / divider
self.uiImage = nil
self.imageWidth = UIScreen.main.bounds.width
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
let size = ImageSizeService.shared.calculateSize(for: attachmentData.url, width: uiImage.size.width, height: uiImage.size.height)
self.imageWidth = size.width
self.imageHeight = size.height
} else if let attachmentData, attachmentData.metaImageWidth > 0 && attachmentData.metaImageHeight > 0 {
let size = ImageSizeService.shared.calculateSize(for: attachmentData.url,
width: attachmentData.metaImageWidth,
height: attachmentData.metaImageHeight)
self.imageWidth = size.width
self.imageHeight = size.height
} else {
self.uiImage = nil
self.imageHeight = UIScreen.main.bounds.width
self.imageHeight = UIScreen.main.bounds.width * 0.75
self.imageWidth = UIScreen.main.bounds.width
}
}

View File

@ -18,19 +18,25 @@ struct ImageRowAsync: View {
init(statusViewModel: StatusViewModel) {
self.statusViewModel = statusViewModel
// Calculate size of frame (first from cache, then from metadata).
if let firstAttachment = statusViewModel.mediaAttachments.first,
let size = ImageSizeService.shared.getImageSize(for: firstAttachment.url) {
self.imageWidth = size.width
self.imageHeight = size.height
self.heightWasPrecalculated = true
} else if let firstAttachment = statusViewModel.mediaAttachments.first,
let imgHeight = (firstAttachment.meta as? ImageMetadata)?.original?.height,
let imgWidth = (firstAttachment.meta as? ImageMetadata)?.original?.width {
let divider = Double(imgWidth) / UIScreen.main.bounds.size.width
let calculatedHeight = Double(imgHeight) / divider
self.imageWidth = UIScreen.main.bounds.width
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
let size = ImageSizeService.shared.calculateSize(for: firstAttachment.url, width: imgWidth, height: imgHeight)
self.imageWidth = size.width
self.imageHeight = size.height
self.heightWasPrecalculated = true
} else {
self.imageWidth = UIScreen.main.bounds.width
self.imageHeight = UIScreen.main.bounds.width
self.imageHeight = UIScreen.main.bounds.width * 0.75
heightWasPrecalculated = false
}
}
@ -94,15 +100,13 @@ struct ImageRowAsync: View {
guard heightWasPrecalculated == false else {
return
}
let imgHeight = imageResponse.image.size.height
let imgWidth = imageResponse.image.size.width
let calculatedHeight = self.calculateHeight(width: imgWidth, height: imgHeight)
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
}
private func calculateHeight(width: Double, height: Double) -> CGFloat {
let divider = width / UIScreen.main.bounds.size.width
return height / divider
if let attachment = statusViewModel.mediaAttachments.first {
let size = ImageSizeService.shared.calculateSize(for: attachment.url,
width: imageResponse.image.size.width,
height: imageResponse.image.size.height)
self.imageWidth = size.width
self.imageHeight = size.height
}
}
}

View File

@ -38,21 +38,33 @@ struct ImagesCarousel: View {
var imgHeight = 0.0
var imgWidth = 0.0
var attachment: AttachmentViewModel?
for item in attachments {
let attachmentheight = Double((item.meta as? ImageMetadata)?.original?.height ?? 0)
if attachmentheight > imgHeight {
attachment = item
imgHeight = attachmentheight
imgWidth = Double((item.meta as? ImageMetadata)?.original?.width ?? 0)
}
}
if imgHeight > 0 && imgWidth > 0 {
let divider = Double(imgWidth) / UIScreen.main.bounds.size.width
let calculatedHeight = Double(imgHeight) / divider
self.imageWidth = UIScreen.main.bounds.width
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
// Attachments doesn't have any metadata with sizes, thus we have to use first one.
if attachment == nil {
attachment = attachments.first
}
// Calculate size of frame (first from cache, then from metadata).
if let attachment, let size = ImageSizeService.shared.getImageSize(for: attachment.url) {
self.imageWidth = size.width
self.imageHeight = size.height
self.heightWasPrecalculated = true
} else if let attachment, imgHeight > 0 && imgWidth > 0 {
let size = ImageSizeService.shared.calculateSize(for: attachment.url, width: imgWidth, height: imgHeight)
self.imageWidth = size.width
self.imageHeight = size.height
self.heightWasPrecalculated = true
} else {
self.imageWidth = UIScreen.main.bounds.width
@ -66,7 +78,7 @@ struct ImagesCarousel: View {
ForEach(attachments, id: \.id) { attachment in
ImageCarouselPicture(attachment: attachment) { (attachment, imageData) in
withAnimation {
self.recalculateImageHeight(imageData: imageData)
self.recalculateImageHeight(attachment: attachment, imageData: imageData)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
@ -94,7 +106,7 @@ struct ImagesCarousel: View {
}
}
private func recalculateImageHeight(imageData: Data) {
private func recalculateImageHeight(attachment: AttachmentViewModel, imageData: Data) {
guard heightWasPrecalculated == false else {
return
}
@ -118,7 +130,8 @@ struct ImagesCarousel: View {
}
}
let divider = imageWidth / UIScreen.main.bounds.size.width
self.imageHeight = imageHeight / divider
let size = ImageSizeService.shared.calculateSize(for: attachment.url, width: imageWidth, height: imageHeight)
self.imageWidth = size.width
self.imageHeight = size.height
}
}

View File

@ -18,9 +18,9 @@ struct UserProfileStatuses: View {
private let defaultLimit = 20
var body: some View {
VStack(alignment: .center) {
LazyVStack(alignment: .center) {
if firstLoadFinished == true {
ForEach(self.statusViewModels, id: \.uniqueId) { item in
ForEach(self.statusViewModels, id: \.id) { item in
NavigationLink(value: RouteurDestinations.status(
id: item.id,
blurhash: item.mediaAttachments.first?.blurhash,
@ -31,24 +31,21 @@ struct UserProfileStatuses: View {
}
.buttonStyle(EmptyButtonStyle())
}
LazyVStack {
if allItemsLoaded == false && firstLoadFinished == true {
HStack {
Spacer()
LoadingIndicator()
.task {
do {
try await self.loadMoreStatuses()
} catch {
ErrorService.shared.handle(error, message: "Loading more statuses failed.", showToastr: true)
}
if allItemsLoaded == false && firstLoadFinished == true {
HStack {
Spacer()
LoadingIndicator()
.task {
do {
try await self.loadMoreStatuses()
} catch {
ErrorService.shared.handle(error, message: "Loading more statuses failed.", showToastr: true)
}
Spacer()
}
}
Spacer()
}
}
} else {
LoadingIndicator()
}