Skip to content

Commit 313560d

Browse files
authored
feat(ios): modernize navigation and settings (#98811)
* feat(ios): modernize navigation and settings * fix(ios): remove obsolete settings helpers * test(ios): update i18n collector fixture * test(ios): update i18n collector fixture * test(ios): update i18n collector fixture
1 parent cc76e06 commit 313560d

19 files changed

Lines changed: 795 additions & 748 deletions

apps/.i18n/native-source.json

Lines changed: 368 additions & 360 deletions
Large diffs are not rendered by default.

apps/ios/Sources/Design/AgentProTab+Destinations.swift

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,35 @@ extension AgentProTab {
2222
}
2323

2424
var agentsDestination: some View {
25-
ZStack {
26-
OpenClawProBackground()
27-
ScrollView {
28-
VStack(alignment: .leading, spacing: 16) {
29-
self.rosterHeader
30-
self.agentFilters
31-
self.agentsSection
25+
List {
26+
Section {
27+
if self.filteredAgents.isEmpty {
28+
self.emptyAgentsRow
29+
} else {
30+
ForEach(self.filteredAgents, id: \.id) { agent in
31+
self.agentRow(agent)
32+
}
3233
}
33-
.padding(.vertical, 18)
3434
}
35-
.refreshable {
36-
await self.refreshOverview(force: true)
35+
}
36+
.listStyle(.insetGrouped)
37+
.navigationTitle(self.headerTitle)
38+
.navigationBarTitleDisplayMode(.large)
39+
.searchable(text: self.$agentSearchText, prompt: "Search agents")
40+
.refreshable {
41+
await self.refreshOverview(force: true)
42+
}
43+
.toolbar {
44+
if let headerLeadingAction {
45+
ToolbarItem(placement: .topBarLeading) {
46+
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
47+
}
48+
}
49+
ToolbarItemGroup(placement: .topBarTrailing) {
50+
self.agentFilterMenu
51+
self.gatewayToolbarButton
3752
}
38-
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
3953
}
40-
// The roster owns its large adaptive header; a second navigation title
41-
// wastes vertical space and makes the phone tab look double-stacked.
42-
.toolbar(.hidden, for: .navigationBar)
4354
}
4455

4556
var skillsDestination: some View {

apps/ios/Sources/Design/AgentProTab+Overview.swift

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,41 @@ extension AgentProTab {
8989
.padding(.horizontal, OpenClawProMetric.pagePadding)
9090
}
9191

92+
var agentFilterMenu: some View {
93+
Menu {
94+
Picker("Agent status", selection: self.$agentRosterFilter) {
95+
ForEach(AgentRosterFilter.allCases) { filter in
96+
Label(filter.title, systemImage: filter.systemImage)
97+
.tag(filter)
98+
}
99+
}
100+
if self.agentFiltersActive {
101+
Divider()
102+
Button("Clear Filters", systemImage: "xmark.circle") {
103+
self.agentRosterFilter = .all
104+
self.agentSearchText = ""
105+
}
106+
}
107+
} label: {
108+
Label("Filter agents", systemImage: "line.3.horizontal.decrease")
109+
.labelStyle(.iconOnly)
110+
}
111+
.accessibilityIdentifier("agent-status-filter-menu")
112+
.accessibilityValue(self.agentRosterFilter.title)
113+
}
114+
115+
@ViewBuilder
116+
var gatewayToolbarButton: some View {
117+
if let openSettings {
118+
Button(action: openSettings) {
119+
Image(systemName: self.gatewayConnected ? "antenna.radiowaves.left.and.right" : "wifi.slash")
120+
}
121+
.tint(self.gatewayConnected ? OpenClawBrand.ok : .secondary)
122+
.accessibilityLabel(self.gatewayConnected ? "Gateway online" : "Gateway offline")
123+
.accessibilityHint("Opens Settings / Gateway")
124+
}
125+
}
126+
92127
var agentFiltersActive: Bool {
93128
self.agentRosterFilter != .all
94129
|| !self.agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
@@ -252,9 +287,7 @@ extension AgentProTab {
252287
.accessibilityHidden(true)
253288
}
254289
}
255-
.padding(.vertical, 10)
256-
.padding(.horizontal, 13)
257-
.frame(maxWidth: .infinity, minHeight: AgentLayout.rowMinHeight, alignment: .leading)
290+
.frame(maxWidth: .infinity, minHeight: 44, alignment: .leading)
258291
.contentShape(Rectangle())
259292
}
260293
.buttonStyle(.plain)
@@ -284,15 +317,15 @@ extension AgentProTab {
284317
.foregroundStyle(.white)
285318
.minimumScaleFactor(0.62)
286319
.lineLimit(1)
287-
.frame(width: 42, height: 42)
320+
.frame(width: 36, height: 36)
288321
.background(
289322
Circle()
290323
.fill(self.agentTint(for: agent, state: state).gradient))
291324
.overlay(Circle().strokeBorder(Color.white.opacity(0.18), lineWidth: 1))
292325

293326
Circle()
294327
.fill(state.color)
295-
.frame(width: 9, height: 9)
328+
.frame(width: 8, height: 8)
296329
.overlay(Circle().strokeBorder(Color(uiColor: .systemBackground), lineWidth: 2))
297330
}
298331
}

apps/ios/Sources/Design/AgentProTab.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,19 @@ struct AgentProTab: View {
7979
case .ready: "Ready"
8080
}
8181
}
82+
83+
var systemImage: String {
84+
switch self {
85+
case .all: "person.2"
86+
case .online: "antenna.radiowaves.left.and.right"
87+
case .ready: "checkmark.circle"
88+
}
89+
}
8290
}
8391

8492
enum AgentLayout {
8593
static let cardRadius: CGFloat = OpenClawProMetric.cardRadius
8694
static let filterHeight: CGFloat = 34
87-
static let rowMinHeight: CGFloat = 72
8895
static let metricTileHeight: CGFloat = 94
8996
}
9097

@@ -175,7 +182,7 @@ struct AgentProTab: View {
175182
private func directDestination(for route: AgentRoute) -> some View {
176183
self.destination(for: route)
177184
.toolbar(
178-
route == .agents || self.directHeaderLeadingAction(for: route) != nil ? .hidden : .visible,
185+
route != .agents && self.directHeaderLeadingAction(for: route) != nil ? .hidden : .visible,
179186
for: .navigationBar)
180187
}
181188
}

apps/ios/Sources/Design/CommandCenterTab.swift

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ struct CommandCenterTab: View {
1111
@State private var defaultChatSessionEntry: OpenClawChatSessionEntry?
1212
@State private var recentChatSessions: [OpenClawChatSessionEntry] = []
1313
var ownsNavigationStack: Bool = true
14+
var usesNativeNavigationChrome: Bool = false
1415
var headerTitle: String = "OpenClaw"
1516
var headerLeadingAction: OpenClawSidebarHeaderAction?
1617
var showsHeaderMark: Bool = true
@@ -57,7 +58,9 @@ struct CommandCenterTab: View {
5758
self.commandAmbientOverlay
5859
ScrollView {
5960
VStack(alignment: .leading, spacing: 14) {
60-
self.header
61+
if !self.usesNativeNavigationChrome {
62+
self.header
63+
}
6164
self.gatewayCard
6265
if Self.usesSplitSectionsLayout(
6366
horizontalSizeClass: self.horizontalSizeClass,
@@ -83,7 +86,19 @@ struct CommandCenterTab: View {
8386
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
8487
}
8588
}
86-
.navigationBarHidden(true)
89+
.navigationTitle(self.headerTitle)
90+
.navigationBarTitleDisplayMode(.inline)
91+
.toolbar(self.usesNativeNavigationChrome ? .visible : .hidden, for: .navigationBar)
92+
.toolbar {
93+
if self.usesNativeNavigationChrome {
94+
ToolbarItem(placement: .topBarTrailing) {
95+
Button(action: self.openSettings) {
96+
Image(systemName: "antenna.radiowaves.left.and.right")
97+
}
98+
.accessibilityLabel("Gateway settings")
99+
}
100+
}
101+
}
87102
}
88103

89104
static func usesSplitSectionsLayout(
@@ -242,7 +257,9 @@ struct CommandCenterTab: View {
242257
.buttonStyle(.plain)
243258
} else {
244259
NavigationLink {
245-
CommandSessionsScreen(openChat: self.openChat)
260+
CommandSessionsScreen(
261+
usesNativeNavigationChrome: self.usesNativeNavigationChrome,
262+
openChat: self.openChat)
246263
} label: {
247264
CommandViewMoreRow()
248265
}
@@ -598,10 +615,16 @@ struct CommandSessionsScreen: View {
598615
@State private var isLoading = false
599616
@State private var loadErrorText: String?
600617
let headerLeadingAction: OpenClawSidebarHeaderAction?
618+
let usesNativeNavigationChrome: Bool
601619
let openChat: () -> Void
602620

603-
init(headerLeadingAction: OpenClawSidebarHeaderAction? = nil, openChat: @escaping () -> Void) {
621+
init(
622+
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
623+
usesNativeNavigationChrome: Bool = false,
624+
openChat: @escaping () -> Void)
625+
{
604626
self.headerLeadingAction = headerLeadingAction
627+
self.usesNativeNavigationChrome = usesNativeNavigationChrome
605628
self.openChat = openChat
606629
}
607630

@@ -610,7 +633,9 @@ struct CommandSessionsScreen: View {
610633
CommandControlBackground()
611634
ScrollView {
612635
VStack(alignment: .leading, spacing: 10) {
613-
self.header
636+
if !self.usesNativeNavigationChrome {
637+
self.header
638+
}
614639
self.sessionsPanel
615640
}
616641
.padding(.top, 16)
@@ -620,6 +645,7 @@ struct CommandSessionsScreen: View {
620645
}
621646
.navigationTitle("Sessions")
622647
.navigationBarTitleDisplayMode(.inline)
648+
.toolbar(self.usesNativeNavigationChrome ? .visible : .hidden, for: .navigationBar)
623649
.task(id: self.refreshID) {
624650
await self.refreshSessions()
625651
}

apps/ios/Sources/Design/IPadActivityScreen.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@ struct IPadActivityScreen: View {
99
@State private var isLoading = false
1010
@State private var loadErrorText: String?
1111
let headerLeadingAction: OpenClawSidebarHeaderAction?
12+
let usesNativeNavigationChrome: Bool
1213
let openChat: () -> Void
1314
let openSettings: () -> Void
1415

1516
init(
1617
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
18+
usesNativeNavigationChrome: Bool = false,
1719
openChat: @escaping () -> Void,
1820
openSettings: @escaping () -> Void)
1921
{
2022
self.headerLeadingAction = headerLeadingAction
23+
self.usesNativeNavigationChrome = usesNativeNavigationChrome
2124
self.openChat = openChat
2225
self.openSettings = openSettings
2326
}
@@ -27,6 +30,7 @@ struct IPadActivityScreen: View {
2730
title: "Activity",
2831
subtitle: "Live device and gateway activity.",
2932
headerLeadingAction: self.headerLeadingAction,
33+
usesNativeNavigationChrome: self.usesNativeNavigationChrome,
3034
gatewayAction: self.openSettings)
3135
{
3236
ProMetricGrid(metrics: self.metrics)

apps/ios/Sources/Design/IPadSidebarScreenChrome.swift

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ struct IPadSidebarScreenChrome<Content: View>: View {
55
let title: String
66
let subtitle: String
77
let headerLeadingAction: OpenClawSidebarHeaderAction?
8+
let usesNativeNavigationChrome: Bool
89
let gatewayAction: (() -> Void)?
910
@ViewBuilder var content: Content
1011

1112
init(
1213
title: String,
1314
subtitle: String,
1415
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
16+
usesNativeNavigationChrome: Bool = false,
1517
gatewayAction: (() -> Void)? = nil,
1618
@ViewBuilder content: () -> Content)
1719
{
1820
self.title = title
1921
self.subtitle = subtitle
2022
self.headerLeadingAction = headerLeadingAction
23+
self.usesNativeNavigationChrome = usesNativeNavigationChrome
2124
self.gatewayAction = gatewayAction
2225
self.content = content()
2326
}
@@ -27,25 +30,40 @@ struct IPadSidebarScreenChrome<Content: View>: View {
2730
OpenClawProBackground()
2831
ScrollView {
2932
VStack(alignment: .leading, spacing: self.isCompactHeight ? 10 : 16) {
30-
OpenClawAdaptiveHeaderRow(
31-
title: self.title,
32-
subtitle: self.subtitle,
33-
titleFont: self.isCompactHeight ? .headline.weight(.semibold) : .title2.weight(.semibold),
34-
subtitleLineLimit: self.isCompactHeight ? 1 : 2)
35-
{
36-
if let headerLeadingAction {
37-
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
33+
if !self.usesNativeNavigationChrome {
34+
OpenClawAdaptiveHeaderRow(
35+
title: self.title,
36+
subtitle: self.subtitle,
37+
titleFont: self.isCompactHeight ? .headline.weight(.semibold) : .title2.weight(.semibold),
38+
subtitleLineLimit: self.isCompactHeight ? 1 : 2)
39+
{
40+
if let headerLeadingAction {
41+
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
42+
}
43+
} accessory: {
44+
self.gatewayPill
3845
}
39-
} accessory: {
40-
self.gatewayPill
46+
.padding(.horizontal, OpenClawProMetric.pagePadding)
4147
}
42-
.padding(.horizontal, OpenClawProMetric.pagePadding)
4348
self.content
4449
}
4550
.padding(.vertical, self.isCompactHeight ? 10 : 18)
4651
}
4752
.safeAreaPadding(.bottom, self.bottomScrollInset)
4853
}
54+
.navigationTitle(self.title)
55+
.navigationBarTitleDisplayMode(.inline)
56+
.toolbar(self.usesNativeNavigationChrome ? .visible : .hidden, for: .navigationBar)
57+
.toolbar {
58+
if self.usesNativeNavigationChrome, let gatewayAction {
59+
ToolbarItem(placement: .topBarTrailing) {
60+
Button(action: gatewayAction) {
61+
Image(systemName: "antenna.radiowaves.left.and.right")
62+
}
63+
.accessibilityLabel("Gateway settings")
64+
}
65+
}
66+
}
4967
}
5068

5169
private var isCompactHeight: Bool {

apps/ios/Sources/Design/IPadSkillWorkshopScreen.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ struct IPadSkillWorkshopScreen: View {
1818
@State private var noticeText: String?
1919
@State private var presentedProposalRoute: IPadSkillProposalSheetRoute?
2020
let headerLeadingAction: OpenClawSidebarHeaderAction?
21+
let usesNativeNavigationChrome: Bool
2122
let openSettings: () -> Void
2223

23-
init(headerLeadingAction: OpenClawSidebarHeaderAction? = nil, openSettings: @escaping () -> Void = {}) {
24+
init(
25+
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
26+
usesNativeNavigationChrome: Bool = false,
27+
openSettings: @escaping () -> Void = {})
28+
{
2429
self.headerLeadingAction = headerLeadingAction
30+
self.usesNativeNavigationChrome = usesNativeNavigationChrome
2531
self.openSettings = openSettings
2632
}
2733

@@ -30,6 +36,7 @@ struct IPadSkillWorkshopScreen: View {
3036
title: "Skill Workshop",
3137
subtitle: "Review and apply proposed skills.",
3238
headerLeadingAction: self.headerLeadingAction,
39+
usesNativeNavigationChrome: self.usesNativeNavigationChrome,
3340
gatewayAction: self.openSettings)
3441
{
3542
if self.isCompactWidth {

apps/ios/Sources/Design/IPadWorkboardScreen.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ struct IPadWorkboardScreen: View {
2121
@State private var dispatchSummaryText: String?
2222
@State private var presentedSheet: IPadWorkboardSheet?
2323
let headerLeadingAction: OpenClawSidebarHeaderAction?
24+
let usesNativeNavigationChrome: Bool
2425
let openChat: () -> Void
2526
let openSettings: () -> Void
2627

2728
init(
2829
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
30+
usesNativeNavigationChrome: Bool = false,
2931
openChat: @escaping () -> Void,
3032
openSettings: @escaping () -> Void = {})
3133
{
3234
self.headerLeadingAction = headerLeadingAction
35+
self.usesNativeNavigationChrome = usesNativeNavigationChrome
3336
self.openChat = openChat
3437
self.openSettings = openSettings
3538
}
@@ -39,6 +42,7 @@ struct IPadWorkboardScreen: View {
3942
title: "Workboard",
4043
subtitle: self.currentWorkboardSubtitle,
4144
headerLeadingAction: self.headerLeadingAction,
45+
usesNativeNavigationChrome: self.usesNativeNavigationChrome,
4246
gatewayAction: self.openSettings)
4347
{
4448
if self.isCompactWidth {

0 commit comments

Comments
 (0)