Skip to content

Commit 3e50f41

Browse files
authored
improve(ios): simplify Talk controls and composer alignment (#98736)
* refactor(ios): streamline talk and composer controls * test(ios): restore Talk control state * chore(ios): refresh native i18n inventory
1 parent 17482a4 commit 3e50f41

9 files changed

Lines changed: 256 additions & 228 deletions

File tree

apps/.i18n/native-source.json

Lines changed: 153 additions & 161 deletions
Large diffs are not rendered by default.

apps/ios/APP-REVIEW-NOTES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ Expected result: the assistant responds by voice. Tap `Stop Talk` when done.
7878
## Talk + Background Audio
7979

8080
1. Tap the `Talk` tab.
81-
2. Confirm `Speakerphone` is on.
82-
3. Confirm `Background listening` is on.
81+
2. Confirm the speaker button is highlighted.
82+
3. Confirm the background-listening button is highlighted.
8383
4. Tap `Start Talk`.
8484
5. If iOS asks for microphone access, tap `Allow`.
8585
6. If iOS asks for Speech Recognition access, tap `Allow`.

apps/ios/Sources/Design/OpenClawBrand.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ enum AppAppearancePreference: String, CaseIterable, Identifiable {
2929
}
3030
}
3131

32-
var detail: String {
33-
switch self {
34-
case .system: "Matches the system appearance."
35-
case .light: "Always uses light appearance."
36-
case .dark: "Always uses dark appearance."
37-
}
38-
}
39-
4032
var colorScheme: ColorScheme? {
4133
switch self {
4234
case .system: nil

apps/ios/Sources/Design/SettingsProTab.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,6 @@ struct SettingsProTab: View {
8989
self.settingsContent))
9090
}
9191

92-
var appearancePreference: AppAppearancePreference {
93-
AppAppearancePreference(rawValue: self.appearancePreferenceRaw) ?? .system
94-
}
95-
9692
@ViewBuilder
9793
private var settingsContent: some View {
9894
if let directRoute {

apps/ios/Sources/Design/SettingsProTabSections.swift

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,13 @@ extension SettingsProTab {
2222
VStack(alignment: .leading, spacing: 8) {
2323
ProSectionHeader(title: "Appearance", uppercase: false)
2424
ProCard(radius: SettingsLayout.cardRadius) {
25-
VStack(alignment: .leading, spacing: 12) {
26-
Picker("Appearance", selection: self.$appearancePreferenceRaw) {
27-
ForEach(AppAppearancePreference.allCases) { preference in
28-
Text(preference.label).tag(preference.rawValue)
29-
}
25+
Picker("Appearance", selection: self.$appearancePreferenceRaw) {
26+
ForEach(AppAppearancePreference.allCases) { preference in
27+
Text(preference.label).tag(preference.rawValue)
3028
}
31-
.pickerStyle(.segmented)
32-
Text(self.appearancePreference.detail)
33-
.font(.caption)
34-
.foregroundStyle(.secondary)
3529
}
30+
.pickerStyle(.segmented)
31+
.accessibilityIdentifier("settings-appearance-picker")
3632
}
3733
.padding(.horizontal, OpenClawProMetric.pagePadding)
3834
}

apps/ios/Sources/Design/TalkProTab.swift

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ struct TalkProTab: View {
9696
.padding(.horizontal, OpenClawProMetric.pagePadding)
9797
}
9898
self.voiceHeroCard
99-
self.controlsCard
99+
self.controlBar
100100
}
101101
.padding(.top, 16)
102102
.padding(.bottom, 18)
@@ -156,48 +156,55 @@ struct TalkProTab: View {
156156
.padding(.horizontal, OpenClawProMetric.pagePadding)
157157
}
158158

159-
private var controlsCard: some View {
160-
CommandPanel(padding: 0) {
161-
VStack(spacing: 0) {
162-
self.controlToggleRow("Speakerphone", isOn: self.talkSpeakerphoneBinding)
163-
Divider().padding(.leading, 14)
164-
self.controlToggleRow("Background listening", isOn: self.$talkBackgroundEnabled)
165-
Divider().padding(.leading, 14)
159+
private var controlBar: some View {
160+
OpenClawGlassControlGroup {
161+
HStack(spacing: 12) {
162+
self.iconToggle(
163+
title: "Speakerphone",
164+
systemImage: self.talkSpeakerphoneEnabled ? "speaker.wave.2.fill" : "speaker.slash.fill",
165+
isOn: self.talkSpeakerphoneBinding,
166+
accessibilityIdentifier: "talk-speakerphone-control")
167+
self.iconToggle(
168+
title: "Background listening",
169+
systemImage: self.talkBackgroundEnabled ? "waveform" : "waveform.slash",
170+
isOn: self.$talkBackgroundEnabled,
171+
accessibilityIdentifier: "talk-background-listening-control")
166172
Button(action: self.openVoiceSettings) {
167-
HStack {
168-
Label("Voice & Talk settings", systemImage: "slider.horizontal.3")
169-
Spacer()
170-
Image(systemName: "chevron.right")
171-
.font(.caption.weight(.bold))
172-
.foregroundStyle(.secondary)
173-
}
174-
.font(.subheadline.weight(.semibold))
175-
.padding(.horizontal, 14)
176-
.padding(.vertical, 12)
173+
Image(systemName: "slider.horizontal.3")
174+
.font(.system(size: 17, weight: .semibold))
175+
.frame(width: 44, height: 44)
177176
}
178-
.buttonStyle(.plain)
177+
.buttonBorderShape(.circle)
178+
.openClawGlassButton()
179+
.accessibilityLabel("Voice & Talk settings")
180+
.accessibilityIdentifier("talk-voice-settings-control")
179181
}
180182
}
183+
.frame(maxWidth: .infinity)
181184
.padding(.horizontal, OpenClawProMetric.pagePadding)
182185
}
183186

184-
private func controlToggleRow(_ title: String, isOn: Binding<Bool>) -> some View {
185-
Toggle(title, isOn: isOn)
186-
.contentShape(Rectangle())
187-
.padding(.horizontal, 14)
188-
.padding(.vertical, 10)
189-
.overlay {
190-
// Keep Toggle semantics for accessibility while making the full visual row tappable.
191-
Button {
192-
isOn.wrappedValue.toggle()
193-
} label: {
194-
Rectangle()
195-
.fill(.clear)
196-
.contentShape(Rectangle())
197-
}
198-
.buttonStyle(.plain)
199-
.accessibilityHidden(true)
200-
}
187+
private func iconToggle(
188+
title: String,
189+
systemImage: String,
190+
isOn: Binding<Bool>,
191+
accessibilityIdentifier: String) -> some View
192+
{
193+
Button {
194+
isOn.wrappedValue.toggle()
195+
} label: {
196+
Image(systemName: systemImage)
197+
.font(.system(size: 17, weight: .semibold))
198+
.contentTransition(.symbolEffect(.replace))
199+
.frame(width: 44, height: 44)
200+
}
201+
.buttonBorderShape(.circle)
202+
.openClawGlassButton(
203+
prominent: isOn.wrappedValue,
204+
tint: isOn.wrappedValue ? Color(uiColor: .systemBlue) : nil)
205+
.accessibilityLabel(title)
206+
.accessibilityValue(isOn.wrappedValue ? "On" : "Off")
207+
.accessibilityIdentifier(accessibilityIdentifier)
201208
}
202209

203210
private var gatewayConnected: Bool {

apps/ios/Tests/OpenClawBrandTests.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@ import UIKit
33
@testable import OpenClaw
44

55
struct OpenClawBrandTests {
6-
@Test func `appearance preference details match selection`() {
7-
#expect(AppAppearancePreference.system.detail == "Matches the system appearance.")
8-
#expect(AppAppearancePreference.light.detail == "Always uses light appearance.")
9-
#expect(AppAppearancePreference.dark.detail == "Always uses dark appearance.")
10-
}
11-
126
@Test func `semantic colors meet text contrast in both appearances`() {
137
let colors = [OpenClawBrand.uiOK, OpenClawBrand.uiWarn, OpenClawBrand.uiInfo]
148
let backgrounds = [UIColor.systemBackground, UIColor.secondarySystemBackground]

apps/ios/UITests/OpenClawSnapshotUITests.swift

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,13 @@ final class OpenClawSnapshotUITests: XCTestCase {
6767
initialDestination: "chat",
6868
name: "chat-composer-growth"))
6969

70-
let textField = try XCTUnwrap(app?.textFields.firstMatch)
70+
let textField = try XCTUnwrap(app?.textFields["chat-message-input"])
7171
XCTAssertTrue(textField.waitForExistence(timeout: 8))
72+
let talkButton = try XCTUnwrap(app?.buttons["chat-realtime-control"])
73+
XCTAssertTrue(talkButton.waitForExistence(timeout: 5))
7274
let compactHeight = textField.frame.height
7375
XCTAssertLessThanOrEqual(compactHeight, 44)
76+
XCTAssertLessThanOrEqual(abs(talkButton.frame.midY - textField.frame.midY), 1)
7477
self.attachScreenshot(named: "chat-composer-compact")
7578

7679
textField.tap()
@@ -86,6 +89,51 @@ final class OpenClawSnapshotUITests: XCTestCase {
8689
XCTAssertTrue(self.app?.keyboards.firstMatch.waitForNonExistence(timeout: 3) == true)
8790
}
8891

92+
func testTalkUsesCompactIconControls() throws {
93+
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone Talk controls only")
94+
self.launchApp(for: ScreenshotTarget(
95+
initialTab: "talk",
96+
initialDestination: "talk",
97+
name: "talk-icon-controls"))
98+
99+
let speakerphone = try XCTUnwrap(app?.buttons["talk-speakerphone-control"])
100+
let backgroundListening = try XCTUnwrap(app?.buttons["talk-background-listening-control"])
101+
let voiceSettings = try XCTUnwrap(app?.buttons["talk-voice-settings-control"])
102+
XCTAssertTrue(speakerphone.waitForExistence(timeout: 8))
103+
XCTAssertTrue(backgroundListening.exists)
104+
XCTAssertTrue(voiceSettings.exists)
105+
XCTAssertFalse(self.app?.switches["Speakerphone"].exists == true)
106+
XCTAssertFalse(self.app?.switches["Background listening"].exists == true)
107+
108+
let originalValue = speakerphone.value as? String
109+
defer {
110+
if speakerphone.value as? String != originalValue {
111+
speakerphone.tap()
112+
}
113+
}
114+
if originalValue == "Off" {
115+
speakerphone.tap()
116+
}
117+
XCTAssertEqual(speakerphone.value as? String, "On")
118+
self.attachScreenshot(named: "talk-icon-controls")
119+
120+
let initialValue = speakerphone.value as? String
121+
speakerphone.tap()
122+
XCTAssertNotEqual(speakerphone.value as? String, initialValue)
123+
}
124+
125+
func testAppearancePickerHasNoRedundantDescription() throws {
126+
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone Settings proof only")
127+
self.launchApp(for: ScreenshotTarget(
128+
initialTab: "settings",
129+
initialDestination: "settings",
130+
name: "appearance-compact"))
131+
132+
XCTAssertTrue(self.app?.segmentedControls["settings-appearance-picker"].waitForExistence(timeout: 8) == true)
133+
XCTAssertFalse(self.app?.staticTexts["Always uses light appearance."].exists == true)
134+
self.attachScreenshot(named: "appearance-compact")
135+
}
136+
89137
func testLiveGatewayControlOverviewNavigation() throws {
90138
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone control hub only")
91139
try XCTSkipUnless(

apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ struct OpenClawChatComposer: View {
336336
HStack(alignment: .bottom, spacing: 8) {
337337
self.compactAccessory(self.attachmentPicker)
338338

339-
HStack(alignment: .bottom, spacing: 8) {
339+
HStack(alignment: .center, spacing: 8) {
340340
self.editorOverlay
341341
.padding(.vertical, self.cleanEditorTextPadding)
342342
.frame(minHeight: self.cleanEditorMinHeight)
@@ -396,6 +396,7 @@ struct OpenClawChatComposer: View {
396396
.disabled(!talkControl.isGatewayConnected && !talkControl.isEnabled)
397397
.accessibilityLabel(talkControl.isEnabled ? "Stop realtime chat" : "Start realtime chat")
398398
.accessibilityValue(self.talkAccessibilityValue(talkControl))
399+
.accessibilityIdentifier("chat-realtime-control")
399400
.help(self.talkHelpText(talkControl))
400401
}
401402

@@ -420,6 +421,7 @@ struct OpenClawChatComposer: View {
420421
.disabled(!talkControl.isGatewayConnected && !talkControl.isEnabled)
421422
.accessibilityLabel(talkControl.isEnabled ? "Stop realtime chat" : "Start realtime chat")
422423
.accessibilityValue(self.talkAccessibilityValue(talkControl))
424+
.accessibilityIdentifier("chat-realtime-control")
423425
.help(self.talkHelpText(talkControl))
424426
}
425427

@@ -528,6 +530,7 @@ struct OpenClawChatComposer: View {
528530
.padding(.vertical, self.composerChrome == .clean ? 0 : 6)
529531
.focused(self.$isFocused)
530532
.disabled(!self.isComposerEnabled)
533+
.accessibilityIdentifier("chat-message-input")
531534
#endif
532535
}
533536
}

0 commit comments

Comments
 (0)