#44 Prefetch images stright after download statuses

This commit is contained in:
Marcin Czachurski 2023-04-22 12:27:18 +02:00
parent c707b136c2
commit 1d0322ecc2
6 changed files with 65 additions and 38 deletions

View File

@ -107,6 +107,18 @@ public extension StatusModel {
}
}
public extension [StatusModel] {
func getAllImagesUrls() -> [URL] {
var urls: [URL] = []
for status in self {
urls.append(contentsOf: status.mediaAttachments.map({ $0.url }))
}
return urls
}
}
public extension [Status] {
func toStatusModels() -> [StatusModel] {
self

View File

@ -9,6 +9,7 @@ import CoreData
import PixelfedKit
import ClientKit
import ServicesKit
import Nuke
/// Service responsible for managing home timeline.
public class HomeTimelineService {
@ -16,6 +17,7 @@ public class HomeTimelineService {
private init() { }
private let defaultAmountOfDownloadedStatuses = 40
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
public func loadOnBottom(for account: AccountModel) async throws -> Int {
// Load data from API and operate on CoreData on background context.
@ -29,26 +31,36 @@ public class HomeTimelineService {
}
// Load data on bottom of the list.
let newStatuses = try await self.load(for: account, on: backgroundContext, maxId: oldestStatus.id)
let allStatusesFromApi = try await self.load(for: account, on: backgroundContext, maxId: oldestStatus.id)
// Save data into database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
// Start prefetching images.
self.prefetch(statuses: allStatusesFromApi)
// Return amount of newly downloaded statuses.
return newStatuses.count
return allStatusesFromApi.count
}
public func refreshTimeline(for account: AccountModel) async throws -> String? {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Retrieve newest visible status (last visible by user).
let dbNewestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext)
let lastSeenStatusId = dbNewestStatus?.rebloggedStatusId ?? dbNewestStatus?.id
// Refresh/load home timeline (refreshing on top downloads always first 40 items).
// When Apple introduce good way to show new items without scroll to top then we can change that method.
let lastSeenStatusId = try await self.refresh(for: account, on: backgroundContext)
let allStatusesFromApi = try await self.refresh(for: account, on: backgroundContext)
// Save data into database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
// Start prefetching images.
self.prefetch(statuses: allStatusesFromApi)
// Return id of last seen status.
return lastSeenStatusId
}
@ -134,19 +146,15 @@ public class HomeTimelineService {
return amountOfStatuses
}
private func refresh(for account: AccountModel, on backgroundContext: NSManagedObjectContext) async throws -> String? {
private func refresh(for account: AccountModel, on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
guard let accessToken = account.accessToken else {
return nil
return []
}
// Retrieve statuses from API.
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(limit: self.defaultAmountOfDownloadedStatuses)
// Retrieve newest visible status (last visible by user).
let dbNewestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext)
let lastSeenStatusId = dbNewestStatus?.rebloggedStatusId ?? dbNewestStatus?.id
// Update all existing statuses in database.
for status in statuses {
if let dbStatus = StatusDataHandler.shared.getStatusData(accountId: account.id, statusId: status.id, viewContext: backgroundContext) {
@ -196,7 +204,8 @@ public class HomeTimelineService {
_ = try await self.add(statusesToAdd, for: account, on: backgroundContext)
}
return lastSeenStatusId
// Return all statuses downloaded from API.
return statuses
}
private func load(for account: AccountModel,
@ -213,13 +222,16 @@ public class HomeTimelineService {
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: self.defaultAmountOfDownloadedStatuses)
// Save statuses in database.
return try await self.add(statuses, for: account, on: backgroundContext)
try await self.add(statuses, for: account, on: backgroundContext)
// Return all statuses downloaded from API.
return statuses
}
private func add(_ statuses: [Status],
for account: AccountModel,
on backgroundContext: NSManagedObjectContext
) async throws -> [Status] {
) async throws {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else {
throw DatabaseError.cannotDownloadAccount
@ -237,8 +249,6 @@ public class HomeTimelineService {
self.copy(from: status, to: statusData, on: backgroundContext)
}
return statusesWithImages
}
private func copy(from status: Status,
@ -287,4 +297,9 @@ public class HomeTimelineService {
}
}
}
private func prefetch(statuses: [Status]) {
let statusModels = statuses.getStatusesWithImagesOnly().toStatusModels()
imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls())
}
}

View File

@ -5,6 +5,7 @@
//
import SwiftUI
import Nuke
import PixelfedKit
import ClientKit
import ServicesKit
@ -38,6 +39,7 @@ struct PaginableStatusesView: View {
@State private var page = 1
private let defaultLimit = 10
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
var body: some View {
self.mainBody()
@ -122,6 +124,9 @@ struct PaginableStatusesView: View {
inPlaceStatuses.append(StatusModel(status: item))
}
// Prefetch images.
self.prefetch(statusModels: inPlaceStatuses)
self.statusViewModels.append(contentsOf: inPlaceStatuses)
}
@ -140,6 +145,9 @@ struct PaginableStatusesView: View {
inPlaceStatuses.append(StatusModel(status: item))
}
// Prefetch images.
self.prefetch(statusModels: inPlaceStatuses)
self.statusViewModels.append(contentsOf: inPlaceStatuses)
}
@ -152,4 +160,8 @@ struct PaginableStatusesView: View {
return try await self.client.accounts?.bookmarks(limit: self.defaultLimit, page: self.page) ?? []
}
}
private func prefetch(statusModels: [StatusModel]) {
imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls())
}
}

View File

@ -5,6 +5,7 @@
//
import SwiftUI
import Nuke
import PixelfedKit
import ClientKit
import ServicesKit
@ -50,6 +51,7 @@ struct StatusesView: View {
@State private var lastStatusId: String?
private let defaultLimit = 20
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
var body: some View {
self.mainBody()
@ -150,6 +152,9 @@ struct StatusesView: View {
inPlaceStatuses.append(StatusModel(status: item))
}
// Prefetch images.
self.prefetch(statusModels: inPlaceStatuses)
// Append to empty list.
self.statusViewModels.append(contentsOf: inPlaceStatuses)
}
@ -174,6 +179,9 @@ struct StatusesView: View {
inPlaceStatuses.append(StatusModel(status: item))
}
// Prefetch images.
self.prefetch(statusModels: inPlaceStatuses)
// Append statuses to existing array of statuses (at the end).
self.statusViewModels.append(contentsOf: inPlaceStatuses)
}
@ -291,4 +299,8 @@ struct StatusesView: View {
ErrorService.shared.handle(error, message: "statuses.error.tagUnfollowFailed", showToastr: true)
}
}
private func prefetch(statusModels: [StatusModel]) {
imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls())
}
}

View File

@ -22,7 +22,6 @@ struct ImageRowItem: View {
@State private var showThumbImage = false
@State private var cancelled = true
@State private var error: Error?
@State private var opacity = 0.0
@State private var isFavourited = false
private let onImageDownloaded: (Double, Double) -> Void
@ -58,21 +57,9 @@ struct ImageRowItem: View {
}
}
}
.opacity(self.opacity)
.onAppear {
withAnimation {
self.opacity = 1.0
}
}
} else {
self.imageContainerView(uiImage: uiImage)
.imageContextMenu(statusData: self.status, attachmentData: self.attachmentData, uiImage: uiImage)
.opacity(self.opacity)
.onAppear {
withAnimation {
self.opacity = 1.0
}
}
}
} else {
if cancelled {

View File

@ -22,7 +22,6 @@ struct ImageRowItemAsync: View {
private let showAvatar: Bool
@State private var showThumbImage = false
@State private var opacity = 0.0
@State private var isFavourited = false
private let onImageDownloaded: (Double, Double) -> Void
@ -61,30 +60,20 @@ struct ImageRowItemAsync: View {
}
}
}
.opacity(self.opacity)
.onAppear {
if let uiImage = state.imageResponse?.image {
self.recalculateSizeOfDownloadedImage(uiImage: uiImage)
}
withAnimation {
self.opacity = 1.0
}
}
} else {
self.imageContainerView(image: image)
.imageContextMenu(statusModel: self.statusViewModel,
attachmentModel: self.attachment,
uiImage: state.imageResponse?.image)
.opacity(self.opacity)
.onAppear {
if let uiImage = state.imageResponse?.image {
self.recalculateSizeOfDownloadedImage(uiImage: uiImage)
}
withAnimation {
self.opacity = 1.0
}
}
}
} else if state.error != nil {