Skip to content

Commit 68075a8

Browse files
committed
enhance: support dynamic number of tabs
1 parent a5badf9 commit 68075a8

File tree

2 files changed

+74
-68
lines changed

2 files changed

+74
-68
lines changed

ios/App/App/LiquidTabsRootView.swift

Lines changed: 73 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,16 @@ struct LiquidTabsRootView: View {
5454
// Controls whether the hidden UITextField should grab keyboard focus.
5555
@State private var hackShowKeyboard: Bool = false
5656

57-
// Native selection type
57+
// Native selection type: dynamic tabs + search
5858
enum TabSelection: Hashable {
59-
case first, second, third, fourth, search
59+
case content(Int) // index into store.tabs
60+
case search
6061
}
6162

62-
@State private var selectedTab: TabSelection = .first
63+
@State private var selectedTab: TabSelection = .content(0)
64+
65+
// (optional) cap number of main tabs if you like
66+
private let maxMainTabs = 6
6367

6468
// MARK: - Re-Tap Logic
6569

@@ -93,44 +97,45 @@ struct LiquidTabsRootView: View {
9397

9498
// MARK: - Tab Helpers
9599

96-
private var firstTab: LiquidTab? { store.tabs.first }
97-
private var secondTab: LiquidTab? { store.tabs.count > 1 ? store.tabs[1] : nil }
98-
private var thirdTab: LiquidTab? { store.tabs.count > 2 ? store.tabs[2] : nil }
99-
private var fourthTab: LiquidTab? { store.tabs.count > 3 ? store.tabs[3] : nil }
100+
private var firstTab: LiquidTab? {
101+
store.tabs.first
102+
}
100103

104+
/// Get tab id for a selection
101105
private func tabId(for selection: TabSelection) -> String? {
102106
switch selection {
103-
case .first: return firstTab?.id
104-
case .second: return secondTab?.id
105-
case .third: return thirdTab?.id
106-
case .fourth: return fourthTab?.id
107-
case .search: return "search"
107+
case .content(let index):
108+
guard index >= 0 && index < store.tabs.count else { return nil }
109+
return store.tabs[index].id
110+
case .search:
111+
return "search"
108112
}
109113
}
110114

111-
115+
/// Map a tab id back to TabSelection
112116
private func selection(forId id: String) -> TabSelection? {
113-
if id == firstTab?.id { return .first }
114-
if id == secondTab?.id { return .second }
115-
if id == thirdTab?.id { return .third }
116-
if id == fourthTab?.id { return .fourth }
117-
if id == "search" { return .search }
117+
if id == "search" {
118+
return .search
119+
}
120+
121+
if let index = store.tabs.firstIndex(where: { $0.id == id }) {
122+
return .content(index)
123+
}
124+
118125
return nil
119126
}
120127

121-
128+
/// Compute initial selection based on store.selectedId or available tabs
122129
private func initialSelection() -> TabSelection {
123-
if let id = store.selectedId {
124-
if id == firstTab?.id { return .first }
125-
if id == secondTab?.id { return .second }
126-
if id == thirdTab?.id { return .third }
127-
if id == fourthTab?.id { return .fourth }
128-
if id == "search" { return .search }
130+
if let id = store.selectedId,
131+
let sel = selection(forId: id) {
132+
return sel
133+
}
134+
135+
if !store.tabs.isEmpty {
136+
return .content(0)
129137
}
130-
if firstTab != nil { return .first }
131-
if secondTab != nil { return .second }
132-
if thirdTab != nil { return .third }
133-
if fourthTab != nil { return .fourth }
138+
134139
return .search
135140
}
136141

@@ -149,36 +154,14 @@ struct LiquidTabsRootView: View {
149154
// Main TabView using the PROXY BINDING
150155
TabView(selection: tabSelectionProxy) {
151156

152-
// ---- Tab 1 ----
153-
if let tab = firstTab {
154-
Tab(tab.title, systemImage: tab.systemImage, value: TabSelection.first) {
155-
NativeNavHost(navController: navController)
156-
.ignoresSafeArea()
157-
.background(Color.logseqBackground)
158-
}
159-
}
160-
161-
// ---- Tab 2 ----
162-
if let tab = secondTab {
163-
Tab(tab.title, systemImage: tab.systemImage, value: TabSelection.second) {
164-
NativeNavHost(navController: navController)
165-
.ignoresSafeArea()
166-
.background(Color.logseqBackground)
167-
}
168-
}
169-
170-
// ---- Tab 3 ----
171-
if let tab = thirdTab {
172-
Tab(tab.title, systemImage: tab.systemImage, value: TabSelection.third) {
173-
NativeNavHost(navController: navController)
174-
.ignoresSafeArea()
175-
.background(Color.logseqBackground)
176-
}
177-
}
178-
179-
// ---- Tab 4 ----
180-
if let tab = fourthTab {
181-
Tab(tab.title, systemImage: tab.systemImage, value: TabSelection.fourth) {
157+
// ---- Dynamic main tabs, using Tab(...) API ----
158+
ForEach(Array(store.tabs.prefix(maxMainTabs).enumerated()),
159+
id: \.element.id) { index, tab in
160+
Tab(
161+
tab.title,
162+
systemImage: tab.systemImage,
163+
value: TabSelection.content(index)
164+
) {
182165
NativeNavHost(navController: navController)
183166
.ignoresSafeArea()
184167
.background(Color.logseqBackground)
@@ -191,7 +174,7 @@ struct LiquidTabsRootView: View {
191174
navController: navController,
192175
isSearchFocused: $isSearchFocused,
193176
selectedTab: $selectedTab,
194-
firstTabId: firstTab?.id,
177+
firstTabId: store.tabs.first?.id,
195178
store: store
196179
)
197180
.ignoresSafeArea()
@@ -216,9 +199,32 @@ struct LiquidTabsRootView: View {
216199
.onAppear {
217200
let initial = initialSelection()
218201
selectedTab = initial
219-
if initial == .search {
202+
if case .search = initial {
220203
isSearchPresented = true
221204
}
205+
206+
let appearance = UITabBarAppearance()
207+
appearance.configureWithOpaqueBackground()
208+
209+
// Background
210+
appearance.backgroundColor = UIColor.logseqBackground
211+
212+
// Selected text color
213+
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
214+
.foregroundColor: UIColor.label
215+
]
216+
217+
// Unselected text color (70%)
218+
let dimmed = UIColor.label.withAlphaComponent(0.7)
219+
appearance.stackedLayoutAppearance.normal.titleTextAttributes = [
220+
.foregroundColor: dimmed
221+
]
222+
223+
// Apply the appearance
224+
let tabBar = UITabBar.appearance()
225+
tabBar.tintColor = .label
226+
tabBar.standardAppearance = appearance
227+
tabBar.scrollEdgeAppearance = appearance
222228
}
223229
// Handle STANDARD tab selection changes
224230
.onChange(of: selectedTab) { newValue in
@@ -227,9 +233,10 @@ struct LiquidTabsRootView: View {
227233
LiquidTabsPlugin.shared?.notifyTabSelected(id: id)
228234
}
229235

230-
if newValue == .search {
236+
switch newValue {
237+
case .search:
231238
isSearchPresented = true
232-
} else {
239+
case .content:
233240
hackShowKeyboard = false
234241
isSearchFocused = false
235242
isSearchPresented = false
@@ -257,7 +264,6 @@ struct LiquidTabsRootView: View {
257264
}
258265

259266
// If it's already selected, treat it as a no-op for programmatic changes
260-
// (if you want programmatic re-tap behavior, you could call handleRetap here)
261267
if newSelection == selectedTab {
262268
return
263269
}
@@ -333,12 +339,12 @@ private struct SearchTabHost: View {
333339
if searching {
334340
wasSearching = true
335341
} else if wasSearching,
336-
selectedTab.wrappedValue == .search,
342+
case .search = selectedTab.wrappedValue,
337343
let firstId = firstTabId {
338344

339-
// Cancel logic - Programmatic switch
345+
// Cancel logic - Programmatic switch back to first content tab
340346
wasSearching = false
341-
selectedTab.wrappedValue = .first
347+
selectedTab.wrappedValue = .content(0)
342348
store.selectedId = firstId
343349
LiquidTabsPlugin.shared?.notifyTabSelected(id: firstId)
344350
}

src/main/mobile/bottom_tabs.cljs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,5 @@
9292
(configure-tabs
9393
[{:id "home" :title "Home" :systemImage "house" :role "normal"}
9494
{:id "favorites" :title "Favorites" :systemImage "star" :role "normal"}
95-
{:id "quick-add" :title "Capture" :systemImage "plus" :role "normal"}
95+
{:id "quick-add" :title "Capture" :systemImage "tray" :role "normal"}
9696
{:id "settings" :title "Settings" :systemImage "gear" :role "normal"}]))

0 commit comments

Comments
 (0)