Skip to content

Commit f5b94af

Browse files
committed
Reduce iOS workspace row swipe contention
1 parent e52dfc1 commit f5b94af

6 files changed

Lines changed: 61 additions & 20 deletions

File tree

Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct DeviceTreeView: View {
3737
/// The active workspace-row filter (All / Unread), the same shared model the
3838
/// flat list uses, applied to every expanded instance's workspace leaves.
3939
@State private var filter: MobileWorkspaceListFilter = .all
40+
@State private var timestampReferenceDate = Date.now
4041

4142
private var expansion: DeviceTreeExpansionStore {
4243
DeviceTreeExpansionStore(storage: expandedStorage)
@@ -85,6 +86,11 @@ struct DeviceTreeView: View {
8586
await store.loadPairedMacs()
8687
await store.loadRegistryDevices()
8788
}
89+
.task {
90+
await WorkspaceRelativeTimestampClock().run {
91+
timestampReferenceDate = $0
92+
}
93+
}
8894
}
8995
.accessibilityIdentifier("MobileDeviceTree")
9096
}
@@ -206,6 +212,7 @@ struct DeviceTreeView: View {
206212
navigationStyle: .push,
207213
wrapWorkspaceTitles: displaySettings.wrapWorkspaceTitles,
208214
previewLineLimit: displaySettings.workspacePreviewLineCount,
215+
timestampReferenceDate: timestampReferenceDate,
209216
selectWorkspace: { id in
210217
selectWorkspace(id)
211218
dismiss()

Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileWorkspacePreview+Display.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ extension MobileWorkspacePreview {
4343

4444
/// The row's trailing slot: the connection problem when there is one,
4545
/// otherwise the compact relative activity time. `now` is threaded from the
46-
/// row's `TimelineView` so the label refreshes as time passes and stays
47-
/// deterministic in tests.
46+
/// row so the label stays deterministic in tests without each row owning a
47+
/// live timer.
4848
func timestampOrStatus(connectionStatus: MobileMacConnectionStatus, now: Date) -> String {
4949
if connectionStatus != .connected {
5050
return connectionStatus.label

Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ struct WorkspaceListView: View {
7070
/// Stored at list scope so reusable rows do not own transient presentation
7171
/// state while `List` is recycling swipe-action rows.
7272
@State private var workspacePendingCloseID: MobileWorkspacePreview.ID?
73+
@State private var timestampReferenceDate = Date.now
7374

7475
private var trimmedQuery: String {
7576
searchText.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -148,6 +149,11 @@ struct WorkspaceListView: View {
148149
}
149150
.listStyle(.plain)
150151
.workspaceListRefreshable(refresh)
152+
.task {
153+
await WorkspaceRelativeTimestampClock().run {
154+
timestampReferenceDate = $0
155+
}
156+
}
151157
.navigationTitle(L10n.string("mobile.workspaces.title", defaultValue: "Workspaces"))
152158
.mobileInlineNavigationTitle()
153159
.searchable(text: $searchText)
@@ -251,6 +257,7 @@ struct WorkspaceListView: View {
251257
navigationStyle: navigationStyle,
252258
wrapWorkspaceTitles: wrapWorkspaceTitles,
253259
previewLineLimit: previewLineLimit,
260+
timestampReferenceDate: timestampReferenceDate,
254261
selectWorkspace: selectWorkspace,
255262
renameWorkspace: renameWorkspace,
256263
setPinned: setPinned,

Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ struct WorkspaceNavigationRow: View {
1111
/// How many lines the activity preview shows (1 or 2), forwarded to the
1212
/// shared ``WorkspaceRow``.
1313
var previewLineLimit: Int = MobileDisplaySettings.defaultWorkspacePreviewLineCount
14+
/// Shared parent-owned reference date for relative activity labels.
15+
var timestampReferenceDate: Date = .now
1416
let selectWorkspace: (MobileWorkspacePreview.ID) -> Void
1517
/// Rename the workspace on the Mac. When `nil` (e.g. previews) the rename
1618
/// affordance is hidden.
@@ -35,16 +37,19 @@ struct WorkspaceNavigationRow: View {
3537
@State private var isRenaming = false
3638

3739
var body: some View {
38-
WorkspaceRow(
39-
workspace: workspace,
40-
connectionStatus: connectionStatus,
41-
isSelected: navigationStyle == .sidebar && isSelected,
42-
wrapWorkspaceTitles: wrapWorkspaceTitles,
43-
previewLineLimit: previewLineLimit
44-
)
45-
.onTapGesture {
40+
Button {
4641
selectWorkspace(workspace.id)
42+
} label: {
43+
WorkspaceRow(
44+
workspace: workspace,
45+
connectionStatus: connectionStatus,
46+
isSelected: navigationStyle == .sidebar && isSelected,
47+
wrapWorkspaceTitles: wrapWorkspaceTitles,
48+
previewLineLimit: previewLineLimit,
49+
timestampReferenceDate: timestampReferenceDate
50+
)
4751
}
52+
.buttonStyle(.plain)
4853
.contentShape(Rectangle())
4954
.contextMenu { contextMenu }
5055
.swipeActions(edge: .leading, allowsFullSwipe: true) {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
struct WorkspaceRelativeTimestampClock {
5+
var interval: Duration = .seconds(60)
6+
7+
@MainActor
8+
func run(update: @escaping @MainActor (Date) -> Void) async {
9+
while !Task.isCancelled {
10+
do {
11+
try await Task.sleep(for: interval)
12+
} catch {
13+
return
14+
}
15+
16+
var transaction = Transaction()
17+
transaction.disablesAnimations = true
18+
withTransaction(transaction) {
19+
update(.now)
20+
}
21+
}
22+
}
23+
}

Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceRow.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import CmuxMobileShellModel
22
import CmuxMobileSupport
3+
import Foundation
34
import SwiftUI
45

56
struct WorkspaceRow: View {
@@ -13,6 +14,10 @@ struct WorkspaceRow: View {
1314
/// "Preview Lines" setting; 2 is the default). Space is reserved so rows
1415
/// with short previews keep the same height as their neighbors.
1516
var previewLineLimit: Int = MobileDisplaySettings.defaultWorkspacePreviewLineCount
17+
/// Passive timestamp reference for the relative activity label. This avoids a
18+
/// per-row `TimelineView` invalidating list rows while UIKit is tracking a
19+
/// native swipe gesture.
20+
var timestampReferenceDate: Date = .now
1621

1722
var body: some View {
1823
HStack(alignment: .top, spacing: 12) {
@@ -40,16 +45,10 @@ struct WorkspaceRow: View {
4045

4146
Spacer(minLength: 8)
4247

43-
// TimelineView re-evaluates the label every minute so a
44-
// quiet row's relative time ("now" -> "1m" -> ...) advances
45-
// without waiting for an unrelated state change to
46-
// invalidate the row. Minute granularity matches the label.
47-
TimelineView(.everyMinute) { context in
48-
Text(workspace.timestampOrStatus(connectionStatus: connectionStatus, now: context.date))
49-
.font(.subheadline)
50-
.foregroundStyle(.secondary)
51-
.lineLimit(1)
52-
}
48+
Text(workspace.timestampOrStatus(connectionStatus: connectionStatus, now: timestampReferenceDate))
49+
.font(.subheadline)
50+
.foregroundStyle(.secondary)
51+
.lineLimit(1)
5352
}
5453

5554
Text(workspace.previewLine)

0 commit comments

Comments
 (0)