diff --git a/Shared/Home/FavoriteItemView.swift b/Shared/Home/FavoriteItemView.swift index 78ca8eb1..d2fa1861 100644 --- a/Shared/Home/FavoriteItemView.swift +++ b/Shared/Home/FavoriteItemView.swift @@ -197,13 +197,22 @@ struct FavoriteItemView: View { } var limitedItems: [ContentItem] { - var items: [ContentItem] + let limit = favoritesModel.limit(item) if item.section == .history { - items = visibleWatches.map { ContentItem(video: player.historyVideo($0.videoID) ?? $0.video) } + return Array(visibleWatches.prefix(limit).map { ContentItem(video: player.historyVideo($0.videoID) ?? $0.video) }) } else { - items = store.contentItems.filter { itemVisible($0) } + var result = [ContentItem]() + result.reserveCapacity(min(store.contentItems.count, limit)) + for contentItem in store.contentItems { + if itemVisible(contentItem) { + result.append(contentItem) + if result.count >= limit { + break + } + } + } + return result } - return Array(items.prefix(favoritesModel.limit(item))) } func itemVisible(_ item: ContentItem) -> Bool { diff --git a/Shared/Home/HomeView.swift b/Shared/Home/HomeView.swift index fe648a46..a450e32d 100644 --- a/Shared/Home/HomeView.swift +++ b/Shared/Home/HomeView.swift @@ -115,30 +115,40 @@ struct HomeView: View { #endif } .onAppear { - Task { - for await _ in Defaults.updates(.favorites) { - favoritesChanged.toggle() - } - for await _ in Defaults.updates(.widgetsSettings) { - favoritesChanged.toggle() - } + updateTask = Task { + async let favoritesUpdates: Void = { + for await _ in Defaults.updates(.favorites) { + favoritesChanged.toggle() + } + }() + async let widgetsUpdates: Void = { + for await _ in Defaults.updates(.widgetsSettings) { + favoritesChanged.toggle() + } + }() + _ = await (favoritesUpdates, widgetsUpdates) } } .onDisappear { updateTask?.cancel() } - .onChange(of: player.presentingPlayer) { _ in - if player.presentingPlayer { + .onChange(of: player.presentingPlayer) { presenting in + if presenting { updateTask?.cancel() } else { - Task { - for await _ in Defaults.updates(.favorites) { - favoritesChanged.toggle() - } - for await _ in Defaults.updates(.widgetsSettings) { - favoritesChanged.toggle() - } + updateTask = Task { + async let favoritesUpdates: Void = { + for await _ in Defaults.updates(.favorites) { + favoritesChanged.toggle() + } + }() + async let widgetsUpdates: Void = { + for await _ in Defaults.updates(.widgetsSettings) { + favoritesChanged.toggle() + } + }() + _ = await (favoritesUpdates, widgetsUpdates) } } } diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 661eb090..e7916909 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -28,7 +28,7 @@ struct VideoPlayerView: View { #endif } - @State private var playerSize: CGSize = .zero { didSet { updateSidebarQueue() } } + @State private var playerSize: CGSize = .zero @State private var hoveringPlayer = false @State private var sidebarQueue = defaultSidebarQueueValue @@ -104,14 +104,16 @@ struct VideoPlayerView: View { content .onAppear { playerSize = geometry.size + updateSidebarQueue() } } .ignoresSafeArea(.all, edges: .bottom) #if os(iOS) .frame(height: playerHeight.isNil ? nil : Double(playerHeight!)) #endif - .onChange(of: geometry.size) { _ in - self.playerSize = geometry.size + .onChange(of: geometry.size) { newSize in + self.playerSize = newSize + updateSidebarQueue() } #if os(iOS) .onChange(of: player.presentingPlayer) { newValue in diff --git a/Shared/Subscriptions/FeedView.swift b/Shared/Subscriptions/FeedView.swift index 12a4a8b0..bec4b1aa 100644 --- a/Shared/Subscriptions/FeedView.swift +++ b/Shared/Subscriptions/FeedView.swift @@ -15,16 +15,27 @@ struct FeedView: View { #endif var videos: [ContentItem] { + let feedVideos = feed.videos guard let selectedChannel else { - return ContentItem.array(of: feed.videos) + return ContentItem.array(of: feedVideos) } - return ContentItem.array(of: feed.videos.filter { - $0.channel.id == selectedChannel.id - }) + return ContentItem.array(of: feedVideos.filter { $0.channel.id == selectedChannel.id }) } var channels: [Channel] { - feed.videos.map(\.channel).unique() + // Optimize by using a Set for uniqueness instead of calling .unique() + var seenIds = Set() + var uniqueChannels = [Channel]() + uniqueChannels.reserveCapacity(feed.videos.count / 10) // Estimate + + for video in feed.videos { + let channelId = video.channel.id + if !seenIds.contains(channelId) { + seenIds.insert(channelId) + uniqueChannels.append(video.channel) + } + } + return uniqueChannels } @State private var selectedChannel: Channel? diff --git a/Shared/Videos/ThumbnailView.swift b/Shared/Videos/ThumbnailView.swift index ed846848..bfdf864c 100644 --- a/Shared/Videos/ThumbnailView.swift +++ b/Shared/Videos/ThumbnailView.swift @@ -5,16 +5,14 @@ import SwiftUI struct ThumbnailView: View { var url: URL? private let thumbnails = ThumbnailsModel.shared + private let thumbnailExtension: String? - var body: some View { - if url != nil { - viewForThumbnailExtension - } else { - placeholder - } + init(url: URL?) { + self.url = url + self.thumbnailExtension = Self.extractExtension(from: url) } - var thumbnailExtension: String? { + private static func extractExtension(from url: URL?) -> String? { guard let url, let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil } @@ -24,6 +22,14 @@ struct ThumbnailView: View { return pathComponents.last } + var body: some View { + if url != nil { + viewForThumbnailExtension + } else { + placeholder + } + } + @ViewBuilder var viewForThumbnailExtension: some View { if AccountsModel.shared.app != .piped, thumbnailExtension != nil { if thumbnailExtension == "webp" { diff --git a/Shared/Videos/VerticalCells.swift b/Shared/Videos/VerticalCells.swift index f9017d40..23e892f4 100644 --- a/Shared/Videos/VerticalCells.swift +++ b/Shared/Videos/VerticalCells.swift @@ -57,7 +57,9 @@ struct VerticalCells: View { } var contentItems: [ContentItem] { - items.isEmpty ? (allowEmpty ? items : ContentItem.placeholders) : items.sorted { $0 < $1 } + // Avoid sorting on every redraw - items should already be sorted from the source + // If sorting is truly needed, it should be done once in the model, not in the view + items.isEmpty ? (allowEmpty ? items : ContentItem.placeholders) : items } func loadMoreContentItemsIfNeeded(current item: ContentItem) { diff --git a/Shared/Videos/VideoCell.swift b/Shared/Videos/VideoCell.swift index f853c62f..4f047f22 100644 --- a/Shared/Videos/VideoCell.swift +++ b/Shared/Videos/VideoCell.swift @@ -475,18 +475,14 @@ struct VideoCell: View { struct VideoCellThumbnail: View { let video: Video - @ObservedObject private var thumbnails = ThumbnailsModel.shared + private var thumbnails: ThumbnailsModel { .shared } var body: some View { - GeometryReader { geometry in - let (url, quality) = thumbnails.best(video) - let aspectRatio = (quality == .default || quality == .high) ? Constants.aspectRatio4x3 : Constants.aspectRatio16x9 + let (url, quality) = thumbnails.best(video) + let aspectRatio = (quality == .default || quality == .high) ? Constants.aspectRatio4x3 : Constants.aspectRatio16x9 - ThumbnailView(url: url) - .aspectRatio(aspectRatio, contentMode: .fill) - .frame(width: geometry.size.width, height: geometry.size.height) - .clipped() - } + ThumbnailView(url: url) + .aspectRatio(aspectRatio, contentMode: .fill) } } diff --git a/Shared/Views/ContentItemView.swift b/Shared/Views/ContentItemView.swift index 416d8ef9..6266b9f9 100644 --- a/Shared/Views/ContentItemView.swift +++ b/Shared/Views/ContentItemView.swift @@ -13,10 +13,21 @@ struct ContentItemView: View { init(item: ContentItem) { self.item = item - if item.contentType == .video, let video = item.video { - _watchRequest = video.watchFetchRequest + // Only create FetchRequest for video items, not for all items + if item.contentType == .video, let videoID = item.video?.videoID { + let predicate = NSPredicate(format: "videoID = %@", videoID as CVarArg) + _watchRequest = FetchRequest( + sortDescriptors: [], + predicate: predicate, + animation: .default + ) } else { - _watchRequest = Video.fixture.watchFetchRequest + // Empty fetch request for non-video items + _watchRequest = FetchRequest( + sortDescriptors: [], + predicate: NSPredicate(value: false), + animation: .default + ) } }