feat: Modernize PhoneTabView with iOS 18/26 adaptive search

Implement version-specific search UI:
- iOS 26+: Dedicated search Tab with .searchable() and role .search
- iOS 18-25: Classic search bar integrated in More tab
- Each main tab now has independent NavigationStack with separate path
- Conditional view switches between menu and search results
- Remove .search from moreTabs array (now integrated)
- Direct binding to SearchBookmarksViewModel.searchQuery
This commit is contained in:
Ilyas Hallak 2025-10-04 00:13:19 +02:00
parent f3d52b3c3a
commit 080c5aa4d2

View File

@ -9,50 +9,178 @@ import SwiftUI
struct PhoneTabView: View { struct PhoneTabView: View {
private let mainTabs: [SidebarTab] = [.all, .unread, .favorite, .archived] private let mainTabs: [SidebarTab] = [.all, .unread, .favorite, .archived]
private let moreTabs: [SidebarTab] = [.search, .article, .videos, .pictures, .tags, .settings] private let moreTabs: [SidebarTab] = [.article, .videos, .pictures, .tags, .settings]
@State private var selectedMoreTab: SidebarTab? = nil @State private var selectedMoreTab: SidebarTab? = nil
@State private var selectedTab: SidebarTab = .unread @State private var selectedTab: SidebarTab = .unread
@State private var offlineBookmarksViewModel = OfflineBookmarksViewModel(syncUseCase: DefaultUseCaseFactory.shared.makeOfflineBookmarkSyncUseCase()) @State private var offlineBookmarksViewModel = OfflineBookmarksViewModel(syncUseCase: DefaultUseCaseFactory.shared.makeOfflineBookmarkSyncUseCase())
// Navigation paths for each tab
@State private var allPath = NavigationPath()
@State private var unreadPath = NavigationPath()
@State private var favoritePath = NavigationPath()
@State private var archivedPath = NavigationPath()
@State private var searchPath = NavigationPath()
@State private var morePath = NavigationPath()
// Search functionality
@State private var searchViewModel = SearchBookmarksViewModel()
@FocusState private var searchFieldIsFocused: Bool
@EnvironmentObject var appSettings: AppSettings @EnvironmentObject var appSettings: AppSettings
var body: some View { var body: some View {
NavigationStack {
GlobalPlayerContainerView { GlobalPlayerContainerView {
TabView { TabView(selection: $selectedTab) {
mainTabsContent
Tab(value: SidebarTab.all) {
NavigationStack(path: $allPath) {
tabView(for: .all)
}
} label: {
Label(SidebarTab.all.label, systemImage: SidebarTab.all.systemImage)
}
Tab(value: SidebarTab.unread) {
NavigationStack(path: $unreadPath) {
tabView(for: .unread)
}
} label: {
Label(SidebarTab.unread.label, systemImage: SidebarTab.unread.systemImage)
}
Tab(value: SidebarTab.favorite) {
NavigationStack(path: $favoritePath) {
tabView(for: .favorite)
}
} label: {
Label(SidebarTab.favorite.label, systemImage: SidebarTab.favorite.systemImage)
}
Tab(value: SidebarTab.archived) {
NavigationStack(path: $archivedPath) {
tabView(for: .archived)
}
} label: {
Label(SidebarTab.archived.label, systemImage: SidebarTab.archived.systemImage)
}
// iOS 26+: Dedicated search tab with role
if #available(iOS 26, *) {
Tab("Search", systemImage: SidebarTab.search.systemImage, value: SidebarTab.search, role: .search) {
NavigationStack {
moreTabContent moreTabContent
} .searchable(text: $searchViewModel.searchQuery, prompt: "Search bookmarks...")
.accentColor(.accentColor)
} }
} }
} .badge(offlineBookmarksViewModel.state.localBookmarkCount > 0 ? offlineBookmarksViewModel.state.localBookmarkCount : 0)
} else {
// MARK: - Tab Content Tab(value: SidebarTab.settings) {
NavigationStack(path: $morePath) {
@ViewBuilder
private var mainTabsContent: some View {
ForEach(mainTabs, id: \.self) { tab in
Tab(tab.label, systemImage: tab.systemImage) {
tabView(for: tab)
}
}
}
@ViewBuilder
private var moreTabContent: some View {
Tab("More", systemImage: "ellipsis") {
VStack(spacing: 0) { VStack(spacing: 0) {
moreTabsList
// Classic search bar for iOS 18
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
TextField("Search...", text: $searchViewModel.searchQuery)
.focused($searchFieldIsFocused)
.textFieldStyle(PlainTextFieldStyle())
.autocapitalization(.none)
.disableAutocorrection(true)
if !searchViewModel.searchQuery.isEmpty {
Button(action: {
searchViewModel.searchQuery = ""
searchFieldIsFocused = true
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray)
}
.buttonStyle(.plain)
}
}
.padding(10)
.background(Color(.systemGray6))
.cornerRadius(12)
.padding([.horizontal, .top])
moreTabContent
moreTabsFooter moreTabsFooter
} }
.navigationTitle("More")
.onAppear { .onAppear {
selectedMoreTab = nil selectedMoreTab = nil
} }
} }
} label: {
Label("More", systemImage: "ellipsis")
}
.badge(offlineBookmarksViewModel.state.localBookmarkCount > 0 ? offlineBookmarksViewModel.state.localBookmarkCount : 0) .badge(offlineBookmarksViewModel.state.localBookmarkCount > 0 ? offlineBookmarksViewModel.state.localBookmarkCount : 0)
} }
}
.accentColor(.accentColor)
// .tabBarMinimizeBehavior(.onScrollDown)
}
}
// MARK: - Tab Content
@ViewBuilder
private var moreTabContent: some View {
if searchViewModel.searchQuery.isEmpty {
moreTabsList
} else {
searchResultsView
}
}
@ViewBuilder
private var searchResultsView: some View {
if searchViewModel.isLoading {
ProgressView("Searching...")
.padding()
} else if let error = searchViewModel.errorMessage {
Text(error)
.foregroundColor(.red)
.padding()
} else if let bookmarks = searchViewModel.bookmarks?.bookmarks, !bookmarks.isEmpty {
List(bookmarks) { bookmark in
ZStack {
NavigationLink {
BookmarkDetailView(bookmarkId: bookmark.id)
.toolbar(.hidden, for: .tabBar)
.navigationBarBackButtonHidden(false)
} label: {
BookmarkCardView(
bookmark: bookmark,
currentState: .all,
layout: appSettings.settings?.cardLayoutStyle ?? .compact,
onArchive: { _ in },
onDelete: { _ in },
onToggleFavorite: { _ in }
)
.listRowBackground(Color(R.color.bookmark_list_bg))
}
.listRowInsets(EdgeInsets(
top: appSettings.settings?.cardLayoutStyle == .compact ? 8 : 12,
leading: 16,
bottom: appSettings.settings?.cardLayoutStyle == .compact ? 8 : 12,
trailing: 16
))
.listRowSeparator(.hidden)
.listRowBackground(Color(R.color.bookmark_list_bg))
}
}
.scrollContentBackground(.hidden)
.background(Color(R.color.bookmark_list_bg))
.listStyle(.plain)
} else if searchViewModel.searchQuery.isEmpty == false {
ContentUnavailableView("No results", systemImage: "magnifyingglass", description: Text("No bookmarks found."))
.padding()
}
}
@ViewBuilder @ViewBuilder
private var moreTabsList: some View { private var moreTabsList: some View {