Polish Search view layout on tvOS

- Disable scroll clipping so focused source/channel/playlist cards show full halo
- Remove rounded clip on source picker row that cut the Menu focus effect
- Replace tappable recents header Button with a plain label on tvOS
- Add vertical spacing between recent search items
- Widen recent channel and playlist cards and reserve space for two-line titles
- Increase horizontal spacing between cards so focus halos don't collide
This commit is contained in:
Arkadiusz Fal
2026-04-17 19:50:04 +02:00
parent 5cbcceba9a
commit 663e96c859
3 changed files with 72 additions and 8 deletions

View File

@@ -22,7 +22,13 @@ struct ChannelCardGridView: View {
private var subscriberFont: Font { isCompact ? .caption2 : .caption }
/// Minimum height for channel name to reserve space for 2 lines
private var titleMinHeight: CGFloat { isCompact ? 32 : 40 }
private var titleMinHeight: CGFloat {
#if os(tvOS)
isCompact ? 56 : 80
#else
isCompact ? 32 : 40
#endif
}
var body: some View {
VStack(alignment: .center, spacing: isCompact ? 8 : 12) {
@@ -46,7 +52,9 @@ struct ChannelCardGridView: View {
.fontWeight(.medium)
.lineLimit(2)
.multilineTextAlignment(.center)
.frame(minHeight: titleMinHeight, alignment: .top)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
.frame(height: titleMinHeight, alignment: .top)
// Subscriber count row - reserve space even when nil
HStack(spacing: 4) {

View File

@@ -17,6 +17,14 @@ struct PlaylistCardView: View {
private var titleFont: Font { isCompact ? .caption : .subheadline }
private var authorFont: Font { isCompact ? .caption2 : .caption }
private var metadataHeight: CGFloat {
#if os(tvOS)
isCompact ? 90 : 110
#else
isCompact ? 50 : 58
#endif
}
var body: some View {
VStack(alignment: .leading, spacing: isCompact ? 4 : 8) {
@@ -61,15 +69,17 @@ struct PlaylistCardView: View {
.fontWeight(.medium)
.lineLimit(2)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
Text(playlist.authorName)
.font(authorFont)
.foregroundStyle(.secondary)
.lineLimit(1)
Spacer(minLength: 0)
}
.frame(height: isCompact ? 50 : 58)
.frame(height: metadataHeight)
}
.contentShape(Rectangle())
}

View File

@@ -362,6 +362,22 @@ struct SearchView: View {
// MARK: - Views
private var recentItemsSpacing: CGFloat {
#if os(tvOS)
16
#else
0
#endif
}
private var horizontalCardSpacing: CGFloat {
#if os(tvOS)
48
#else
16
#endif
}
/// Instance picker for selecting search source when multiple instances are available
@ViewBuilder
private var instancePickerView: some View {
@@ -393,7 +409,9 @@ struct SearchView: View {
}
.padding(listStyle == .inset ? 4 : 0)
.background(listStyle == .inset ? ListBackgroundStyle.card.color : .clear)
#if !os(tvOS)
.clipShape(.rect(cornerRadius: 10))
#endif
.padding(.horizontal)
}
}
@@ -580,6 +598,16 @@ struct SearchView: View {
if !searchHistory.isEmpty {
VStack(alignment: .leading, spacing: 0) {
// Header - tappable to expand/collapse when more than 5 items
#if os(tvOS)
HStack {
Text(String(localized: "search.recentSearches.title"))
.font(.headline)
.foregroundStyle(.primary)
Spacer()
}
.padding(.horizontal)
.padding(.bottom, 8)
#else
Button {
if hasMoreSearchHistory {
withAnimation(.easeInOut(duration: 0.2)) {
@@ -604,9 +632,10 @@ struct SearchView: View {
.disabled(!hasMoreSearchHistory)
.padding(.horizontal)
.padding(.bottom, 8)
#endif
// Items
VStack(spacing: 0) {
VStack(spacing: recentItemsSpacing) {
ForEach(displayedSearchHistory) { history in
Button {
executeSearch(history.query)
@@ -674,7 +703,7 @@ struct SearchView: View {
// Horizontal scroll
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16) {
LazyHStack(spacing: horizontalCardSpacing) {
ForEach(recentChannels) { recentChannel in
NavigationLink(
value: NavigationDestination.channel(
@@ -689,7 +718,11 @@ struct SearchView: View {
channel: channelFromRecent(recentChannel),
isCompact: false
)
#if os(tvOS)
.frame(width: 240)
#else
.frame(width: 160)
#endif
}
.zoomTransitionSource(id: recentChannel.channelID)
.buttonStyle(.plain)
@@ -704,6 +737,9 @@ struct SearchView: View {
}
.padding(.horizontal)
}
#if os(tvOS)
.scrollClipDisabled()
#endif
}
}
@@ -720,14 +756,18 @@ struct SearchView: View {
// Horizontal scroll
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16) {
LazyHStack(spacing: horizontalCardSpacing) {
ForEach(recentPlaylists) { recentPlaylist in
NavigationLink(value: NavigationDestination.playlist(.remote(playlistIDFromRecent(recentPlaylist), instance: nil, title: recentPlaylist.title))) {
PlaylistCardView(
playlist: playlistFromRecent(recentPlaylist),
isCompact: false
)
#if os(tvOS)
.frame(width: 320)
#else
.frame(width: 200)
#endif
}
.zoomTransitionSource(id: playlistIDFromRecent(recentPlaylist))
.buttonStyle(.plain)
@@ -742,6 +782,9 @@ struct SearchView: View {
}
.padding(.horizontal)
}
#if os(tvOS)
.scrollClipDisabled()
#endif
}
}
@@ -763,6 +806,9 @@ struct SearchView: View {
}
.padding(.vertical)
}
#if os(tvOS)
.scrollClipDisabled()
#endif
.background(listStyle == .inset ? ListBackgroundStyle.grouped.color : ListBackgroundStyle.plain.color)
.accessibilityLabel("search.recents")
.confirmationDialog(