enhance(ux): iOS search toolbar

This commit is contained in:
Tienson Qin
2025-11-17 18:56:24 +08:00
parent 152986f84e
commit 4b34769afd
4 changed files with 115 additions and 64 deletions

View File

@@ -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 = "<group>"; };
D3989CBF2ECB0E5700D06615 /* LiquidTabsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidTabsStore.swift; sourceTree = "<group>"; };
D3989CC02ECB0E5700D06615 /* NativeNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeNavHost.swift; sourceTree = "<group>"; };
D3989CC12ECB0E5700D06615 /* SearchTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTabView.swift; sourceTree = "<group>"; };
D3989CC72ECB174A00D06615 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
D39D1FDF2E7DAFB000C903D1 /* LogseqIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogseqIntents.swift; sourceTree = "<group>"; };
D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = "<group>"; };
@@ -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 */,

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}