|
42 | 42 | adapter = DiffableAdapter(view, section: 0, dataSource: dataSource) |
43 | 43 | } |
44 | 44 |
|
| 45 | + /// Highlights the issue the detail shows, except on an iPhone, where |
| 46 | + /// the list and the issue aren't side by side. |
| 47 | + func select(_ issueID: String?) { |
| 48 | + guard traitCollection.horizontalSizeClass != .compact else { return } |
| 49 | + let indexPath = issueID.flatMap { issueID in |
| 50 | + adapter?.rows.first { $0.value.id == issueID } |
| 51 | + .flatMap { dataSource?.indexPath(for: $0.key) } |
| 52 | + } |
| 53 | + if indexPath != tableView.indexPathForSelectedRow { |
| 54 | + tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) |
| 55 | + } |
| 56 | + } |
| 57 | + |
45 | 58 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
46 | 59 | guard let id = dataSource?.itemIdentifier(for: indexPath), |
47 | 60 | let issue = adapter?.row(for: id) |
|
53 | 66 | struct IssueTable: UIViewControllerRepresentable { |
54 | 67 | let zero: Zero<AuthData?> |
55 | 68 | let filter: IssueFilter |
56 | | - @Binding var selection: String? |
| 69 | + /// The issue the detail shows. |
| 70 | + let selection: String? |
| 71 | + /// Opens an issue, even the one the detail shows. |
| 72 | + let open: (String) -> Void |
57 | 73 |
|
58 | 74 | func makeUIViewController(context: Context) -> IssueTableController { |
59 | 75 | IssueTableController(style: .plain) |
60 | 76 | } |
61 | 77 |
|
62 | 78 | func updateUIViewController(_ controller: IssueTableController, context: Context) { |
63 | | - controller.onSelect = { selection = $0 } |
| 79 | + controller.onSelect = open |
64 | 80 | controller.show(zero, filter) |
| 81 | + controller.select(selection) |
65 | 82 | } |
66 | 83 | } |
67 | 84 | #elseif canImport(AppKit) |
|
74 | 91 | private var adapter: DiffableAdapter<Issue, Int>? |
75 | 92 | private var dataSource: NSTableViewDiffableDataSource<Int, RowID>? |
76 | 93 | private var key: (zero: ObjectIdentifier, filter: IssueFilter)? |
| 94 | + /// Whether `select` is changing the selection, which isn't the user |
| 95 | + /// choosing an issue. |
| 96 | + private var selecting = false |
77 | 97 |
|
78 | 98 | override func loadView() { |
79 | 99 | let column = NSTableColumn(identifier: .init("issue")) |
|
86 | 106 | dataSource = NSTableViewDiffableDataSource(tableView: tableView) { |
87 | 107 | [unowned self] _, _, _, id in |
88 | 108 | let cell = NSTableCellView() |
89 | | - cell.setAccessibilityIdentifier("IssueCell") |
| 109 | + let icon = NSImageView() |
| 110 | + icon.symbolConfiguration = .init(textStyle: .headline) |
90 | 111 | let title = NSTextField(labelWithString: "") |
91 | 112 | title.font = .preferredFont(forTextStyle: .headline) |
92 | 113 | title.lineBreakMode = .byTruncatingTail |
| 114 | + // The cell view's identifier doesn't reach the accessibility tree. |
| 115 | + title.setAccessibilityIdentifier("IssueCell") |
93 | 116 | let subtitle = NSTextField(labelWithString: "") |
94 | 117 | subtitle.font = .preferredFont(forTextStyle: .caption1) |
95 | 118 | subtitle.textColor = .secondaryLabelColor |
| 119 | + subtitle.lineBreakMode = .byTruncatingTail |
| 120 | + // Truncated, not clipped, in a column narrower than the text. |
| 121 | + for label in [title, subtitle] { |
| 122 | + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) |
| 123 | + } |
96 | 124 | if let issue = adapter?.row(for: id) { |
97 | | - title.stringValue = (issue.open ? "○ " : "✓ ") + issue.title |
| 125 | + icon.image = NSImage( |
| 126 | + systemSymbolName: issue.open ? "circle" : "checkmark.circle.fill", |
| 127 | + accessibilityDescription: issue.open ? "Open" : "Closed") |
| 128 | + icon.contentTintColor = issue.open ? .systemGreen : .systemPurple |
| 129 | + title.stringValue = issue.title |
98 | 130 | subtitle.stringValue = issue.subtitle |
99 | 131 | } |
100 | | - let stack = NSStackView(views: [title, subtitle]) |
| 132 | + // So a selected row draws them in its colors. |
| 133 | + cell.imageView = icon |
| 134 | + cell.textField = title |
| 135 | + let heading = NSStackView(views: [icon, title]) |
| 136 | + heading.spacing = 6 |
| 137 | + let stack = NSStackView(views: [heading, subtitle]) |
101 | 138 | stack.orientation = .vertical |
102 | 139 | stack.alignment = .leading |
103 | 140 | stack.spacing = 2 |
|
127 | 164 | adapter = DiffableAdapter(view, section: 0, dataSource: dataSource) |
128 | 165 | } |
129 | 166 |
|
| 167 | + /// Highlights the issue the detail shows. |
| 168 | + func select(_ issueID: String?) { |
| 169 | + let row = issueID.flatMap { issueID in |
| 170 | + adapter?.rows.first { $0.value.id == issueID } |
| 171 | + .flatMap { dataSource?.snapshot().indexOfItem($0.key) } |
| 172 | + } |
| 173 | + guard (row ?? -1) != tableView.selectedRow else { return } |
| 174 | + selecting = true |
| 175 | + defer { selecting = false } |
| 176 | + if let row { |
| 177 | + tableView.selectRowIndexes([row], byExtendingSelection: false) |
| 178 | + } else { |
| 179 | + tableView.deselectAll(nil) |
| 180 | + } |
| 181 | + } |
| 182 | + |
130 | 183 | func tableViewSelectionDidChange(_ notification: Notification) { |
131 | | - guard tableView.selectedRow >= 0, |
| 184 | + guard !selecting, tableView.selectedRow >= 0, |
132 | 185 | let id = dataSource?.itemIdentifier(forRow: tableView.selectedRow), |
133 | 186 | let issue = adapter?.row(for: id) |
134 | 187 | else { return } |
|
139 | 192 | struct IssueTable: NSViewControllerRepresentable { |
140 | 193 | let zero: Zero<AuthData?> |
141 | 194 | let filter: IssueFilter |
142 | | - @Binding var selection: String? |
| 195 | + /// The issue the detail shows. |
| 196 | + let selection: String? |
| 197 | + /// Opens an issue. |
| 198 | + let open: (String) -> Void |
143 | 199 |
|
144 | 200 | func makeNSViewController(context: Context) -> IssueTableController { |
145 | 201 | IssueTableController() |
146 | 202 | } |
147 | 203 |
|
148 | 204 | func updateNSViewController(_ controller: IssueTableController, context: Context) { |
149 | | - controller.onSelect = { selection = $0 } |
| 205 | + controller.onSelect = open |
150 | 206 | controller.show(zero, filter) |
| 207 | + controller.select(selection) |
151 | 208 | } |
152 | 209 | } |
153 | 210 | #endif |
|
0 commit comments