Watch: demo screenshots, wrapping reply chips, feed toolbar and inset fixes, README
Screenshots of Pair, Feed, Thread (finished, approval, question) and New thread, taken in the Series 12 46mm simulator in demo mode. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bdfb79b891
commit
9731fd621f
10 files changed
+103
-3
No files matched your search
@@ -0,0 +1,41 @@
|
||||
# WristCode watch app
|
||||
|
||||
SwiftUI, watch only, Swift 6 with strict concurrency, and no dependencies. It talks to the gateway
|
||||
over HTTPS + JSON as described in [docs/api.md](../docs/api.md); `WristCode/API/Models.swift`
|
||||
mirrors [gateway/src/contract.ts](../gateway/src/contract.ts).
|
||||
|
||||
## Build and test
|
||||
|
||||
```sh
|
||||
cd watch
|
||||
xcodegen generate # the .xcodeproj is generated and gitignored
|
||||
xcodebuild -scheme WristCode -destination 'platform=watchOS Simulator,name=Apple Watch Series 12 (46mm)' build
|
||||
xcodebuild -scheme WristCode -destination 'platform=watchOS Simulator,name=Apple Watch Series 12 (46mm)' test
|
||||
```
|
||||
|
||||
## Demo mode
|
||||
|
||||
`DemoGateway` serves fixture data shaped like the docs/api.md examples, and it reacts to writes:
|
||||
Allow moves a thread to working, a reply restarts a finished thread. The SwiftUI previews use it,
|
||||
the pairing screen has a "Try the demo" button, and launch arguments open a screen directly:
|
||||
|
||||
```sh
|
||||
xcrun simctl launch <udid> org.gbrown.wristcode -demo # feed
|
||||
xcrun simctl launch <udid> org.gbrown.wristcode -demo -thread littleman/t-next # thread
|
||||
xcrun simctl launch <udid> org.gbrown.wristcode -demo -screen new # new thread
|
||||
```
|
||||
|
||||
`screenshots/` holds the screens captured this way.
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Job |
|
||||
| --- | --- |
|
||||
| `WristCodeApp.swift` | App entry, root navigation, `WKApplicationDelegate` for APNs registration |
|
||||
| `AppModel.swift` | Paired, demo or unpaired; navigation path; the feed; 401 sends you back to pairing |
|
||||
| `API/GatewayClient.swift` | `GatewayAPI` protocol and the URLSession client |
|
||||
| `API/Models.swift` | Codable mirror of the contract |
|
||||
| `Auth/DeviceStore.swift` | Device token and gateway URL in the Keychain |
|
||||
| `Demo/DemoGateway.swift` | In-memory gateway with fixtures |
|
||||
| `Views/` | Pair, Feed, Thread, New thread, and the mock B theme |
|
||||
| `Notifications/NotificationHandler.swift` | Categories `approval`, `finished`, `failed`, `input`, `host` and their actions |
|
||||
@@ -41,6 +41,8 @@ struct FeedView: View {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
NavigationLink(value: Route.newThread) {
|
||||
Image(systemName: "plus")
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.black)
|
||||
}
|
||||
.accessibilityLabel("New thread")
|
||||
}
|
||||
@@ -61,7 +63,7 @@ struct FeedView: View {
|
||||
private extension View {
|
||||
func feedRow() -> some View {
|
||||
listRowBackground(Color.clear)
|
||||
.listRowInsets(EdgeInsets(top: 3, leading: 0, bottom: 3, trailing: 0))
|
||||
.listRowInsets(EdgeInsets(top: 3, leading: 4, bottom: 3, trailing: 4))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,3 +110,60 @@ enum Format {
|
||||
return words.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
/// Lays children out left to right and wraps them onto new lines, like the mock's reply chips.
|
||||
struct FlowLayout: Layout {
|
||||
var spacing: CGFloat = 6
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
let rows = arrange(width: proposal.width ?? .infinity, subviews: subviews)
|
||||
return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: rows.last.map { $0.y + $0.height } ?? 0)
|
||||
}
|
||||
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
||||
for row in arrange(width: bounds.width, subviews: subviews) {
|
||||
var x = bounds.minX
|
||||
for index in row.indices {
|
||||
let size = subviews[index].sizeThatFits(.unspecified)
|
||||
subviews[index].place(at: CGPoint(x: x, y: bounds.minY + row.y), proposal: ProposedViewSize(size))
|
||||
x += size.width + spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Row {
|
||||
var indices: [Int] = []
|
||||
var y: CGFloat = 0
|
||||
var width: CGFloat = 0
|
||||
var height: CGFloat = 0
|
||||
}
|
||||
|
||||
private func arrange(width: CGFloat, subviews: Subviews) -> [Row] {
|
||||
var rows: [Row] = []
|
||||
var row = Row()
|
||||
for index in subviews.indices {
|
||||
let size = subviews[index].sizeThatFits(.unspecified)
|
||||
if !row.indices.isEmpty, row.width + spacing + size.width > width {
|
||||
rows.append(row)
|
||||
row = Row(y: row.y + row.height + spacing)
|
||||
}
|
||||
row.width += (row.indices.isEmpty ? 0 : spacing) + size.width
|
||||
row.height = max(row.height, size.height)
|
||||
row.indices.append(index)
|
||||
}
|
||||
if !row.indices.isEmpty { rows.append(row) }
|
||||
return rows
|
||||
}
|
||||
}
|
||||
|
||||
/// A content-sized chip, for one-tap replies.
|
||||
struct ChipButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.system(size: 13, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(.white)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 10)
|
||||
.background(Theme.card.opacity(configuration.isPressed ? 0.6 : 1), in: Capsule())
|
||||
}
|
||||
}
|
||||
@@ -278,10 +278,10 @@ private struct QuickReplies: View {
|
||||
var send: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 6) {
|
||||
FlowLayout {
|
||||
ForEach(["Yes", "Continue", "Commit it"], id: \.self) { reply in
|
||||
Button(reply) { send(reply) }
|
||||
.buttonStyle(ChoiceButtonStyle(fill: Theme.card))
|
||||
.buttonStyle(ChipButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Reference in new issue
Block a user