From a0a54bced9bcee6be118feda8c72538766a61b3d Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 14 Nov 2025 20:02:07 +0100 Subject: [PATCH] Improve layout stability and disable unwanted animations Added height reservation to FavoriteItemView to prevent layout shifts during content loading. Changed HomeView to use LazyVStack for better performance. Converted QueueView from LazyVStack to VStack. Disabled animations on content count changes across multiple views to prevent jarring layout transitions. Added width constraint to stream button in PlaybackSettings. --- Shared/Home/FavoriteItemView.swift | 55 +++++++++--- Shared/Home/HomeView.swift | 122 ++++++++++++++------------- Shared/Home/QueueView.swift | 2 +- Shared/Player/PlaybackSettings.swift | 2 +- Shared/Videos/HorizontalCells.swift | 1 + Shared/Videos/ListView.swift | 3 +- 6 files changed, 108 insertions(+), 77 deletions(-) diff --git a/Shared/Home/FavoriteItemView.swift b/Shared/Home/FavoriteItemView.swift index 9002c19b..aa8f9fd2 100644 --- a/Shared/Home/FavoriteItemView.swift +++ b/Shared/Home/FavoriteItemView.swift @@ -34,7 +34,7 @@ struct FavoriteItemView: View { var body: some View { Group { if isVisible { - VStack(alignment: .leading, spacing: 2) { + VStack(alignment: .leading, spacing: 0) { itemControl .contextMenu { contextMenu } .contentShape(Rectangle()) @@ -64,24 +64,34 @@ struct FavoriteItemView: View { #else .padding(.horizontal, 15) #endif + .frame(height: expectedContentHeight) } else { - Group { - switch widgetListingStyle { - case .horizontalCells: - HorizontalCells(items: limitedItems) - case .list: - ListView(items: limitedItems) - .padding(.vertical, 10) - #if os(tvOS) - .padding(.leading, 40) - #else - .padding(.horizontal, 15) - #endif + ZStack(alignment: .topLeading) { + // Reserve space immediately to prevent layout shift + Color.clear + .frame(height: expectedContentHeight) + + // Actual content renders within the reserved space + Group { + switch widgetListingStyle { + case .horizontalCells: + HorizontalCells(items: limitedItems) + case .list: + ListView(items: limitedItems) + .padding(.vertical, 10) + #if os(tvOS) + .padding(.leading, 40) + #else + .padding(.horizontal, 15) + #endif + } } + .environment(\.inChannelView, inChannelView) } - .environment(\.inChannelView, inChannelView) + .animation(nil, value: store.contentItems.count) } } + .animation(nil, value: store.contentItems.count) .contentShape(Rectangle()) .onAppear { if item.section == .history { @@ -233,6 +243,23 @@ struct FavoriteItemView: View { favoritesModel.listingStyle(item) } + var expectedContentHeight: Double { + switch widgetListingStyle { + case .horizontalCells: + #if os(tvOS) + return 600 + #else + return 290 + #endif + case .list: + // Approximate height for list view items + let itemCount = favoritesModel.limit(item) + let itemHeight: Double = 70 // Approximate height per item + let padding: Double = 20 + return Double(itemCount) * itemHeight + padding + } + } + func loadCacheAndResource(force: Bool = false) { guard let resource else { return } diff --git a/Shared/Home/HomeView.swift b/Shared/Home/HomeView.swift index 3632207c..13b59a07 100644 --- a/Shared/Home/HomeView.swift +++ b/Shared/Home/HomeView.swift @@ -30,91 +30,93 @@ struct HomeView: View { var body: some View { ScrollView(.vertical, showsIndicators: false) { - VStack { - #if !os(tvOS) - HStack { - if showOpenActionsInHome { - AccentButton(text: "Files", imageSystemName: "folder") { - NavigationModel.shared.presentingFileImporter = true - } - AccentButton(text: "Paste", imageSystemName: "doc.on.clipboard.fill") { - OpenVideosModel.shared.openURLsFromClipboard(playbackMode: .playNow) - } - AccentButton(imageSystemName: "ellipsis") { - NavigationModel.shared.presentingOpenVideos = true - } - .frame(maxWidth: 40) - } - } - #endif - - #if os(tvOS) - HStack { - if showOpenActionsInHome { - Button { - NavigationModel.shared.presentingOpenVideos = true - } label: { - Label("Open Video", systemImage: "globe") + LazyVStack(spacing: 0, pinnedViews: []) { + VStack { + #if !os(tvOS) + HStack { + if showOpenActionsInHome { + AccentButton(text: "Files", imageSystemName: "folder") { + NavigationModel.shared.presentingFileImporter = true + } + AccentButton(text: "Paste", imageSystemName: "doc.on.clipboard.fill") { + OpenVideosModel.shared.openURLsFromClipboard(playbackMode: .playNow) + } + AccentButton(imageSystemName: "ellipsis") { + NavigationModel.shared.presentingOpenVideos = true + } + .frame(maxWidth: 40) } } - Button { - NavigationModel.shared.presentingAccounts = true - } label: { - Label("Locations", systemImage: "globe") - } - Spacer() - HideWatchedButtons() - HideShortsButtons() - Button { - NavigationModel.shared.presentingSettings = true - } label: { - Label("Settings", systemImage: "gear") - } - } - #if os(tvOS) - .font(.caption) - .imageScale(.small) - .foregroundColor(.primary) #endif - #endif - } - .padding(.top, 15) - #if os(tvOS) - .padding(.horizontal, 40) - #else - .padding(.horizontal, 15) - #endif - if showQueueInHome { - QueueView() + #if os(tvOS) + HStack { + if showOpenActionsInHome { + Button { + NavigationModel.shared.presentingOpenVideos = true + } label: { + Label("Open Video", systemImage: "globe") + } + } + Button { + NavigationModel.shared.presentingAccounts = true + } label: { + Label("Locations", systemImage: "globe") + } + Spacer() + HideWatchedButtons() + HideShortsButtons() + Button { + NavigationModel.shared.presentingSettings = true + } label: { + Label("Settings", systemImage: "gear") + } + } + .font(.caption) + .imageScale(.small) + .foregroundColor(.primary) + #endif + } + .padding(.top, 15) + .padding(.bottom, 15) #if os(tvOS) .padding(.horizontal, 40) #else .padding(.horizontal, 15) #endif - } - if !accounts.current.isNil, showFavoritesInHome { - VStack(alignment: .leading) { + if showQueueInHome { + QueueView() + #if os(tvOS) + .padding(.horizontal, 40) + #else + .padding(.horizontal, 15) + #endif + } + + if !accounts.current.isNil, showFavoritesInHome { #if os(tvOS) ForEach(Defaults[.favorites]) { item in FavoriteItemView(item: item, favoritesChanged: $favoritesChanged) + .animation(nil, value: favoritesChanged) } #else ForEach(favorites) { item in FavoriteItemView(item: item, favoritesChanged: $favoritesChanged) + .animation(nil, value: favoritesChanged) #if os(macOS) .workaroundForVerticalScrollingBug() #endif } #endif } - } - #if !os(tvOS) - Color.clear.padding(.bottom, 60) - #endif + #if !os(tvOS) + Color.clear.padding(.bottom, 60) + #endif + } } + .animation(nil, value: favoritesChanged) .onAppear { updateTask = Task { async let favoritesUpdates: Void = { diff --git a/Shared/Home/QueueView.swift b/Shared/Home/QueueView.swift index 298126d2..052cfef6 100644 --- a/Shared/Home/QueueView.swift +++ b/Shared/Home/QueueView.swift @@ -6,7 +6,7 @@ struct QueueView: View { @ObservedObject private var player = PlayerModel.shared var body: some View { - LazyVStack { + VStack { if !items.isEmpty { Button { withAnimation { diff --git a/Shared/Player/PlaybackSettings.swift b/Shared/Player/PlaybackSettings.swift index b3fdc275..eee698d4 100644 --- a/Shared/Player/PlaybackSettings.swift +++ b/Shared/Player/PlaybackSettings.swift @@ -411,7 +411,7 @@ struct PlaybackSettings: View { } .transaction { t in t.animation = .none } .buttonStyle(.plain) - .frame(height: 40, alignment: .trailing) + .frame(width: 140, height: 40, alignment: .trailing) #else StreamControl(focusedField: $focusedField) #endif diff --git a/Shared/Videos/HorizontalCells.swift b/Shared/Videos/HorizontalCells.swift index 069f5f59..129a57d4 100644 --- a/Shared/Videos/HorizontalCells.swift +++ b/Shared/Videos/HorizontalCells.swift @@ -34,6 +34,7 @@ struct HorizontalCells: View { } .frame(height: cellHeight) .edgesIgnoringSafeArea(.horizontal) + .animation(nil, value: contentItems.count) } var contentItems: [ContentItem] { diff --git a/Shared/Videos/ListView.swift b/Shared/Videos/ListView.swift index 8ed10b35..6a82fe15 100644 --- a/Shared/Videos/ListView.swift +++ b/Shared/Videos/ListView.swift @@ -10,9 +10,10 @@ struct ListView: View { ContentItemView(item: item) .environment(\.listingStyle, .list) .environment(\.noListingDividers, limit == 1) - .transition(.opacity) + .transition(.identity) } } + .animation(nil, value: limitedItems.count) } var limitedItems: [ContentItem] {