Skip to content

Commit 0f9c1b9

Browse files
authored
Add Observation support (#490)
1 parent a8b4397 commit 0f9c1b9

File tree

18 files changed

+316
-104
lines changed

18 files changed

+316
-104
lines changed

.github/workflows/claude-code-review.yml

Lines changed: 0 additions & 54 deletions
This file was deleted.

.github/workflows/issue-triage.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Issue Triage
2+
on:
3+
issues:
4+
types: [opened]
5+
6+
jobs:
7+
triage-issue:
8+
runs-on: ubuntu-latest
9+
timeout-minutes: 10
10+
permissions:
11+
contents: read
12+
issues: write
13+
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Triage issue with Claude
21+
uses: anthropics/claude-code-action@v1
22+
with:
23+
prompt: |
24+
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
25+
26+
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
27+
28+
Issue Information:
29+
- REPO: ${{ github.repository }}
30+
- ISSUE_NUMBER: ${{ github.event.issue.number }}
31+
32+
TASK OVERVIEW:
33+
34+
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
35+
36+
2. Next, use the GitHub tools to get context about the issue:
37+
- You have access to these tools:
38+
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
39+
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
40+
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
41+
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
42+
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
43+
- Start by using mcp__github__get_issue to get the issue details
44+
45+
3. Analyze the issue content, considering:
46+
- The issue title and description
47+
- The type of issue (bug report, feature request, question, etc.)
48+
- Technical areas mentioned
49+
- Severity or priority indicators
50+
- User impact
51+
- Components affected
52+
53+
4. Select appropriate labels from the available labels list provided above:
54+
- Choose labels that accurately reflect the issue's nature
55+
- Be specific but comprehensive
56+
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
57+
- Consider platform labels (android, ios) if applicable
58+
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
59+
60+
5. Apply the selected labels:
61+
- Use mcp__github__update_issue to apply your selected labels
62+
- DO NOT post any comments explaining your decision
63+
- DO NOT communicate directly with users
64+
- If no labels are clearly applicable, do not apply any labels
65+
66+
IMPORTANT GUIDELINES:
67+
- Be thorough in your analysis
68+
- Only select labels from the provided list above
69+
- DO NOT post any comments to the issue
70+
- Your ONLY action should be to apply labels using mcp__github__update_issue
71+
- It's okay to not add any labels if none are clearly applicable
72+
73+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
74+
github_token: ${{ secrets.GITHUB_TOKEN }}
75+
claude_args: |
76+
--allowedTools "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"

CLAUDE.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,11 @@ The Package.swift heavily uses environment variables for conditional compilation
136136

137137
### Commit Workflow
138138
When asked to commit changes:
139-
1. **Generate a commit message** and provide it to the user
140-
2. **List the files to be committed** (specify if not all files)
141-
3. **Let the user commit through GitBulter GUI** - do not run git commit commands
142-
4. Wait for user confirmation before proceeding with any PR creation
139+
1. **Generate a commit message** and provide it to the user (formatted without extra spaces)
140+
2. **Save the commit message** to `.claude/tmp/commit-message.md` for easy copying
141+
3. **List the files to be committed** (specify if not all files)
142+
4. **Let the user commit through GitBulter GUI** - do not run git commit commands
143+
5. Wait for user confirmation before proceeding with any PR creation
143144

144145
Example response when asked to commit:
145146
```
@@ -154,6 +155,8 @@ Here's the commit message for you to use in GitBulter:
154155
Files to commit:
155156
- src/feature.swift
156157
- tests/feature_test.swift
158+
159+
(Saved to .claude/tmp/commit-message.md)
157160
```
158161

159162
### Pull Request Workflow

Example/HostingExample/ViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ class ViewController: NSViewController {
6666

6767
struct ContentView: View {
6868
var body: some View {
69-
GeometryReaderExample()
69+
ObservationExample()
7070
}
7171
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// ObservationExample.swift
3+
// SharedExample
4+
5+
#if OPENSWIFTUI
6+
import OpenObservation
7+
import OpenSwiftUI
8+
#else
9+
import Observation
10+
import SwiftUI
11+
#endif
12+
13+
@Observable
14+
private class Model {
15+
var showRed = false
16+
}
17+
18+
struct ObservationExample: View {
19+
@State private var model = Model()
20+
21+
private var showRed: Bool {
22+
get { model.showRed }
23+
nonmutating set { model.showRed = newValue }
24+
}
25+
26+
var body: some View {
27+
VStack {
28+
Color(platformColor: showRed ? .red : .blue)
29+
.frame(width: showRed ? 200 : 400, height: showRed ? 200 : 400)
30+
}
31+
.animation(.spring, value: showRed)
32+
.onAppear {
33+
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
34+
showRed.toggle()
35+
}
36+
}
37+
}
38+
}

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -232,24 +232,25 @@ struct PlatformViewChild<Content: PlatformViewRepresentable>: StatefulRule {
232232
}
233233
if platformView == nil {
234234
let host = ViewGraph.viewRendererHost
235-
// TODO: StatefulRule.withObservation
236-
Graph.withoutUpdate {
237-
let representableContext = PlatformViewRepresentableContext<Content>(
238-
coordinator: coordinator!,
239-
preferenceBridge: bridge,
240-
transaction: transaction,
241-
environmentStorage: .eager(environment)
242-
)
243-
representableContext.values.asCurrent {
244-
let provider = view.makeViewProvider(context: representableContext)
245-
let environment = environment.removingTracker()
246-
platformView = PlatformViewHost(
247-
provider,
248-
host: host,
249-
environment: environment,
250-
viewPhase: phase,
251-
importer: importer
235+
withObservation {
236+
Graph.withoutUpdate {
237+
let representableContext = PlatformViewRepresentableContext<Content>(
238+
coordinator: coordinator!,
239+
preferenceBridge: bridge,
240+
transaction: transaction,
241+
environmentStorage: .eager(environment)
252242
)
243+
representableContext.values.asCurrent {
244+
let provider = view.makeViewProvider(context: representableContext)
245+
let environment = environment.removingTracker()
246+
platformView = PlatformViewHost(
247+
provider,
248+
host: host,
249+
environment: environment,
250+
viewPhase: phase,
251+
importer: importer
252+
)
253+
}
253254
}
254255
}
255256
}

Sources/OpenSwiftUI/Util/AnyAttributeFix.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ package typealias AttributeType = OpenSwiftUICore.AttributeType
1313
extension AnyAttribute {
1414
package static var `nil`: AnyAttribute { AnyAttribute(rawValue: 0x2) }
1515

16+
package static var currentWasModified: Bool { false }
17+
1618
package var source: AnyAttribute? {
1719
get { preconditionFailure("#39") }
1820
nonmutating set { preconditionFailure("#39") }

Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,11 @@ private struct StaticBody<Accessor: BodyAccessor, ThreadFlags: RuleThreadFlags>
309309
extension StaticBody: StatefulRule {
310310
typealias Value = Accessor.Body
311311

312+
// Audited with 6.5.4
312313
func updateValue() {
313-
accessor.updateBody(of: container, changed: true)
314+
withObservation {
315+
accessor.updateBody(of: container, changed: true)
316+
}
314317
}
315318

316319
static var flags: Flags { ThreadFlags.value }
@@ -370,19 +373,26 @@ private struct DynamicBody<Accessor: BodyAccessor, ThreadFlags: RuleThreadFlags>
370373
extension DynamicBody: StatefulRule {
371374
typealias Value = Accessor.Body
372375

376+
// Audited with 6.5.4
373377
mutating func updateValue() {
374378
if resetSeed != phase.resetSeed {
375379
links.reset()
376380
resetSeed = phase.resetSeed
377381
}
378-
var (container, containerChanged) = $container.changedValue()
379-
let linkChanged = withUnsafeMutablePointer(to: &container) {
380-
links.update(container: $0, phase: phase)
382+
var (container, changed) = $container.changedValue()
383+
withObservation {
384+
withUnsafeMutablePointer(to: &container) {
385+
if links.update(container: $0, phase: phase) {
386+
changed = true
387+
}
388+
}
389+
accessor.updateBody(
390+
of: container,
391+
changed: changed || !hasValue || AnyAttribute.currentWasModified
392+
)
381393
}
382-
let changed = linkChanged || containerChanged || !hasValue
383-
accessor.updateBody(of: container, changed: changed)
384394
}
385-
395+
386396
static var flags: Flags { ThreadFlags.value }
387397
}
388398

Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyTransformModifier.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// OpenSwiftUICore
44
//
55
// Audited for iOS 18.0
6-
// Status: Blocked by Observation
6+
// Status: Complete
77
// ID: 1DBD4F024EFF0E73A70DB6DD05D5B548 (SwiftUI)
88
// ID: E370275CDB55AC7AD9ACF0420859A9E8 (SwiftUICore)
99

@@ -63,14 +63,14 @@ private struct ChildEnvironment<Value>: StatefulRule, AsyncAttribute, CustomStri
6363

6464
typealias Value = EnvironmentValues
6565

66-
// FIXME
6766
mutating func updateValue() {
6867
var (environment, environmentChanged) = _environment.changedValue()
6968
let keyPath = modifier.keyPath
7069
var newValue = environment[keyPath: keyPath]
7170
$modifier.syncMainIfReferences { modifier in
72-
// TODO: Observation
73-
modifier.transform(&newValue)
71+
withObservation {
72+
modifier.transform(&newValue)
73+
}
7474
}
7575
guard !environmentChanged,
7676
let valueChanged = oldValue.map({ compareValues($0, newValue, mode: .equatableUnlessPOD) }), !valueChanged,

0 commit comments

Comments
 (0)