Improve loading statuses on home timeline.

This commit is contained in:
Marcin Czachursk 2023-01-10 07:16:54 +01:00
parent ea3713d80d
commit 476e515423
5 changed files with 83 additions and 31 deletions

View File

@ -25,7 +25,6 @@
F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; };
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
F8341F90295C636C009C8EE6 /* Data+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* Data+Exif.swift */; };
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; };
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; };
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; };
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4972296406E700751DF7 /* BottomRight.swift */; };
@ -105,7 +104,6 @@
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>"; };
F8341F8F295C636C009C8EE6 /* Data+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Exif.swift"; sourceTree = "<group>"; };
F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = "<group>"; };
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; };
F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = "<group>"; };
F85D4972296406E700751DF7 /* BottomRight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomRight.swift; sourceTree = "<group>"; };
@ -235,7 +233,6 @@
isa = PBXGroup;
children = (
F8C14399296B2150001FE31D /* Errors */,
F8341F91295C63BB009C8EE6 /* ImageStatus.swift */,
F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */,
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */,
F8C14397296B208A001FE31D /* HTTPStatusCode.swift */,
@ -493,7 +490,6 @@
F85DBF8F296732E20069BF89 /* FollowersView.swift in Sources */,
F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */,
F85D49872964334100751DF7 /* String+Date.swift in Sources */,
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */,
F897978829681B9C00B22335 /* UserAvatar.swift in Sources */,
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */,
F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */,

View File

@ -1,15 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
import UIKit
import MastodonSwift
public struct ImageStatus: Identifiable {
public let id: String
public let image: UIImage
public let status: Status
}

View File

@ -57,12 +57,26 @@ public class TimelineService {
// Retrieve statuses from API.
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 40)
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 20)
// Download all images from server.
let attachmentsData = await self.fetchAllImages(statuses: statuses)
// Save status data in database.
for status in statuses {
let contains = attachmentsData.contains { (key: String, value: Data) in
status.mediaAttachments.contains { attachment in
attachment.id == key
}
}
// We are adding status only when we have at least one image for status.
if contains == false {
continue
}
let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext)
try await self.copy(from: status, to: statusData, on: backgroundContext)
try await self.copy(from: status, to: statusData, attachmentsData: attachmentsData, on: backgroundContext)
}
try backgroundContext.save()
@ -72,20 +86,21 @@ public class TimelineService {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Download all images from server.
let attachmentsData = await self.fetchAllImages(statuses: [status])
// Update status data in database.
try await self.copy(from: status, to: statusData, on: backgroundContext)
try await self.copy(from: status, to: statusData, attachmentsData: attachmentsData, on: backgroundContext)
try backgroundContext.save()
return statusData
}
private func copy(from status: Status, to statusData: StatusData, on backgroundContext: NSManagedObjectContext) async throws {
private func copy(from status: Status, to statusData: StatusData, attachmentsData: Dictionary<String, Data>, on backgroundContext: NSManagedObjectContext) async throws {
statusData.copyFrom(status)
for attachment in status.mediaAttachments {
let imageData = try await self.fetchImage(attachment: attachment)
guard let imageData = imageData else {
guard let imageData = attachmentsData[attachment.id] else {
continue
}
@ -127,8 +142,46 @@ public class TimelineService {
}
}
private func fetchImage(attachment: Attachment) async throws -> Data? {
guard let data = try await RemoteFileService.shared.fetchData(url: attachment.url) else {
private func fetchAllImages(statuses: [Status]) async -> Dictionary<String, Data> {
var attachmentUrls: Dictionary<String, URL> = [:]
statuses.forEach { status in
status.mediaAttachments.forEach { attachment in
attachmentUrls[attachment.id] = attachment.url
}
}
return await withTaskGroup(of: (String, Data?).self, returning: [String : Data].self) { taskGroup in
for attachmentUrl in attachmentUrls {
taskGroup.addTask {
do {
if let imageData = try await self.fetchImage(attachmentUrl: attachmentUrl.value) {
return (attachmentUrl.key, imageData)
}
return (attachmentUrl.key, nil)
} catch {
print("Error \(error.localizedDescription)")
return (attachmentUrl.key, nil)
}
}
}
var childTaskResults = [String: Data]()
for await result in taskGroup {
guard let data = result.1 else {
continue
}
childTaskResults[result.0] = data
}
return childTaskResults
}
}
private func fetchImage(attachmentUrl: URL) async throws -> Data? {
guard let data = try await RemoteFileService.shared.fetchData(url: attachmentUrl) else {
return nil
}

View File

@ -8,8 +8,10 @@ import SwiftUI
struct ImageRow: View {
@State public var status: StatusData
@State private var showSensitive = false
@State private var imageHeight = UIScreen.main.bounds.width
@State private var imageWidth = UIScreen.main.bounds.width
var body: some View {
if let attachmenData = self.status.attachments().first,
let uiImage = UIImage(data: attachmenData.data) {
@ -39,8 +41,24 @@ struct ImageRow: View {
}.padding()
}
}
.frame(width: self.imageWidth, height: self.imageHeight)
.onAppear {
self.recalculateSizeOfDownloadedImage(uiImage: uiImage)
}
}
}
private func recalculateSizeOfDownloadedImage(uiImage: UIImage) {
let imgHeight = uiImage.size.height
let imgWidth = uiImage.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
}
}
struct ImageRow_Previews: PreviewProvider {

View File

@ -10,10 +10,10 @@ import NukeUI
struct ImageRowAsync: View {
@State public var status: Status
@State private var imageHeight = UIScreen.main.bounds.width
@State private var imageWidth = UIScreen.main.bounds.width
@State private var heightWasPrecalculated = true
@State private var showSensitive = false
var body: some View {
if let attachment = status.mediaAttachments.first {