From 4b34769afdb59f8a5482acfec4d66da4073782f0 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Mon, 17 Nov 2025 18:56:24 +0800 Subject: [PATCH] enhance(ux): iOS search toolbar --- ios/App/App.xcodeproj/project.pbxproj | 4 - ios/App/App/LiquidTabsRootView.swift | 135 +++++++++++++++++++++----- ios/App/App/SearchTabView.swift | 30 ------ src/main/mobile/bottom_tabs.cljs | 10 +- 4 files changed, 115 insertions(+), 64 deletions(-) delete mode 100644 ios/App/App/SearchTabView.swift diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index e7b41be425..67515d23ee 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -35,7 +35,6 @@ D3989CC32ECB0E5700D06615 /* LiquidTabsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CBF2ECB0E5700D06615 /* LiquidTabsStore.swift */; }; D3989CC42ECB0E5700D06615 /* LiquidTabsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CBD2ECB0E5700D06615 /* LiquidTabsPlugin.swift */; }; D3989CC52ECB0E5700D06615 /* NativeNavHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CC02ECB0E5700D06615 /* NativeNavHost.swift */; }; - D3989CC62ECB0E5700D06615 /* SearchTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CC12ECB0E5700D06615 /* SearchTabView.swift */; }; D3989CC82ECB174A00D06615 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CC72ECB174A00D06615 /* SceneDelegate.swift */; }; D39D1FE02E7DAFB000C903D1 /* LogseqIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D1FDF2E7DAFB000C903D1 /* LogseqIntents.swift */; }; D39D1FE12E7DAFB000C903D1 /* LogseqIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D1FDF2E7DAFB000C903D1 /* LogseqIntents.swift */; }; @@ -116,7 +115,6 @@ D3989CBE2ECB0E5700D06615 /* LiquidTabsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidTabsRootView.swift; sourceTree = ""; }; D3989CBF2ECB0E5700D06615 /* LiquidTabsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidTabsStore.swift; sourceTree = ""; }; D3989CC02ECB0E5700D06615 /* NativeNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeNavHost.swift; sourceTree = ""; }; - D3989CC12ECB0E5700D06615 /* SearchTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTabView.swift; sourceTree = ""; }; D3989CC72ECB174A00D06615 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; D39D1FDF2E7DAFB000C903D1 /* LogseqIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogseqIntents.swift; sourceTree = ""; }; D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = ""; }; @@ -201,7 +199,6 @@ D3989CBE2ECB0E5700D06615 /* LiquidTabsRootView.swift */, D3989CBF2ECB0E5700D06615 /* LiquidTabsStore.swift */, D3989CC02ECB0E5700D06615 /* NativeNavHost.swift */, - D3989CC12ECB0E5700D06615 /* SearchTabView.swift */, D39D1FDF2E7DAFB000C903D1 /* LogseqIntents.swift */, A1B2C3D41E2F3A4B5C6D7E8F /* NativePageViewController.swift */, A1B2C3D41E2F3A4B5C6D7E91 /* SharedWebViewController.swift */, @@ -453,7 +450,6 @@ D3989CC32ECB0E5700D06615 /* LiquidTabsStore.swift in Sources */, D3989CC42ECB0E5700D06615 /* LiquidTabsPlugin.swift in Sources */, D3989CC52ECB0E5700D06615 /* NativeNavHost.swift in Sources */, - D3989CC62ECB0E5700D06615 /* SearchTabView.swift in Sources */, D3989CC82ECB174A00D06615 /* SceneDelegate.swift in Sources */, 5FF8632A283B5ADB0047731B /* Utils.swift in Sources */, CBF2D2E22DE95970006338BE /* AppViewController.swift in Sources */, diff --git a/ios/App/App/LiquidTabsRootView.swift b/ios/App/App/LiquidTabsRootView.swift index d9914299e1..2a772e4089 100644 --- a/ios/App/App/LiquidTabsRootView.swift +++ b/ios/App/App/LiquidTabsRootView.swift @@ -4,46 +4,129 @@ struct LiquidTabsRootView: View { @StateObject private var store = LiquidTabsStore.shared let navController: UINavigationController + @State private var searchText: String = "" + + // Convenience helpers: first two tabs from CLJS, rest ignored + private var firstTab: LiquidTab? { + store.tabs.first + } + + private var secondTab: LiquidTab? { + store.tabs.count > 1 ? store.tabs[1] : nil + } + + private var thirdTab: LiquidTab? { + store.tabs.count > 2 ? store.tabs[2] : nil + } + var body: some View { - Group { + if #available(iOS 26.0, *) { + // iOS 26+: static TabView with a dedicated search tab (role: .search) if store.tabs.isEmpty { // Fallback: just show your existing nav + webview NativeNavHost(navController: navController) - .ignoresSafeArea() + .ignoresSafeArea() } else { - TabView(selection: Binding( - get: { store.effectiveSelectedId() }, - set: { newValue in - guard let id = newValue else { return } - store.selectedId = id - LiquidTabsPlugin.shared?.notifyTabSelected(id: id) + TabView { + // ---- Tab 1 (normal) ---- + if let tab = firstTab { + Tab { + NativeNavHost(navController: navController) + .ignoresSafeArea() + .onAppear { + store.selectedId = tab.id + LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) + } + } label: { + Label(tab.title, systemImage: tab.systemImage) + } } - )) { - ForEach(store.tabs) { tab in - tabView(for: tab) + + // ---- Tab 2 (optional normal) ---- + if let tab = secondTab { + Tab { + NativeNavHost(navController: navController) + .ignoresSafeArea() + .onAppear { + store.selectedId = tab.id + LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) + } + } label: { + Label(tab.title, systemImage: tab.systemImage) + } } + + if let tab = thirdTab { + Tab { + NativeNavHost(navController: navController) + .ignoresSafeArea() + .onAppear { + store.selectedId = tab.id + LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) + } + } label: { + Label(tab.title, systemImage: tab.systemImage) + } + } + + // ---- Search tab (static, special pill) ---- + Tab(role: .search) { + SearchView(searchText: $searchText) + .onAppear { + // Use a fixed id for search, or map it from CLJS if you prefer + LiquidTabsPlugin.shared?.notifyTabSelected(id: "search") + } + } + }} + + } else { + // iOS < 26: fall back to the old dynamic tabItem-based tabs + TabView(selection: Binding( + get: { store.selectedId ?? firstTab?.id }, + set: { newValue in + guard let id = newValue else { return } + store.selectedId = id + LiquidTabsPlugin.shared?.notifyTabSelected(id: id) + } + )) { + ForEach(store.tabs) { tab in + NativeNavHost(navController: navController) + .ignoresSafeArea() + .tabItem { + Label(tab.title, systemImage: tab.systemImage) + } + .tag(tab.id as String?) } } } } +} - @ViewBuilder - private func tabView(for tab: LiquidTab) -> some View { - switch tab.role { - case .normal: - NativeNavHost(navController: navController) - .ignoresSafeArea() - .tabItem { - Label(tab.title, systemImage: tab.systemImage) - } - .tag(tab.id as String?) - case .search: - SearchTabView() - .tabItem { - Label(tab.title, systemImage: tab.systemImage) +struct SearchView: View { + @Binding var searchText: String + + var body: some View { + if #available(iOS 26.0, *) { + NavigationStack { + if #available(iOS 17.0, *) { + ContentUnavailableView("Search", systemImage: "magnifyingglass") + .navigationTitle("Search") + } else { + Text("Search") } - .tag(tab.id as String?) + } + .searchable( + text: $searchText, + placement: .automatic, + prompt: "Search" + ) + .searchToolbarBehavior(.minimize) // Liquid behavior on iOS 26 + .onChange(of: searchText) { newValue in + LiquidTabsPlugin.shared?.notifySearchChanged(query: newValue) + } + } else { + // Fallback on earlier versions } } } diff --git a/ios/App/App/SearchTabView.swift b/ios/App/App/SearchTabView.swift deleted file mode 100644 index 312f1dbf53..0000000000 --- a/ios/App/App/SearchTabView.swift +++ /dev/null @@ -1,30 +0,0 @@ -import SwiftUI - -struct SearchTabView: View { - @State private var searchText: String = "" - - var body: some View { - if #available(iOS 26.0, *) { - NavigationStack { - // Placeholder content – your web app will react to search anyway - if #available(iOS 17.0, *) { - ContentUnavailableView("Search", systemImage: "magnifyingglass") - .navigationTitle("Search") - } else { - // Fallback on earlier versions - } - } - .searchable( - text: $searchText, - placement: .automatic, - prompt: "Search" - ) - .searchToolbarBehavior(.minimize) - .onChange(of: searchText) { newValue in - LiquidTabsPlugin.shared?.notifySearchChanged(query: newValue) - } - } else { - // Fallback on earlier versions - } - } -} diff --git a/src/main/mobile/bottom_tabs.cljs b/src/main/mobile/bottom_tabs.cljs index 4d64223612..156d4f8d1a 100644 --- a/src/main/mobile/bottom_tabs.cljs +++ b/src/main/mobile/bottom_tabs.cljs @@ -61,12 +61,14 @@ [] (p/do! (configure-tabs - [{:id "home" :title "Home" :systemImage "house" :role "normal"} - {:id "quick-add" :title "Capture" :systemImage "plus" :role "normal"} - {:id "settings" :title "Settings" :systemImage "gear" :role "normal"} - {:id "search" :title "Search" :systemImage "magnifyingglass" :role "search"}]) + [{:id "home" :title "Home" :systemImage "house" :role "normal"} + {:id "quick-add" :title "Capture" :systemImage "plus" :role "normal"} + {:id "settings" :title "Settings" :systemImage "gear" :role "normal"} + ;; {:id "search" :title "Search" :systemImage "magnifyingglass" :role "search"} + ]) (add-tab-selected-listener! (fn [tab] + (prn :debug :tab tab) (when-not (= tab "quick-add") (mobile-state/set-tab! tab)) (case tab