pointfreeco / swift-perception

Observable tools, backported.
MIT License
554 stars 40 forks source link

Picker binding triggers runtime warnings in iOS 18 #100

Open watt opened 2 weeks ago

watt commented 2 weeks ago

Description

A SwiftUI Picker bound to a @Perceptible model triggers the "Perceptible state was accessed but is not being tracked" runtime warnings in iOS 18.

It seems to be due to access to the selection binding from the resolved body style. Example stack trace below.

Example stack trace ``` #0 0x0000000103593d9c in PerceptionRegistrar.perceptionCheck(fileID:filePath:line:column:) at /Users/awatt/Development/swift-perception/Sources/Perception/PerceptionRegistrar.swift:214 #1 0x0000000103593ac0 in PerceptionRegistrar.access(_:keyPath:fileID:filePath:line:column:) at /Users/awatt/Development/swift-perception/Sources/Perception/PerceptionRegistrar.swift:88 #2 0x0000000103580948 in OptionModel.access(keyPath:fileID:filePath:line:column:) at /var/folders/zj/tw5xtpzn7jq3lhcj08zq18br0000gn/T/swift-generated-sources/@__swiftmacro_7Example11OptionModel11PerceptiblefMm_.swift:10 #3 0x0000000103580420 in OptionModel.option.getter at /var/folders/zj/tw5xtpzn7jq3lhcj08zq18br0000gn/T/swift-generated-sources/@__swiftmacro_7Example11OptionModelC6option17PerceptionTrackedfMa_.swift:7 #4 0x00000001035802fc in key path getter for OptionModel.option : OptionModel () #5 0x00000001948594d4 in function signature specialization of project2<τ_0_0, τ_0_1><τ_1_0><τ_2_0>(τ_2_0.Type) -> Swift.Optional<τ_0_1> () #6 0x00000001948590e0 in Swift.KeyPath._projectReadOnly(from: τ_0_0) -> τ_0_1 () #7 0x000000019485cc40 in swift_getAtKeyPath () #8 0x0000000194a366ec in swift_readAtKeyPath () #9 0x00000001d2f5efb0 in SwiftUI.ObjectLocation.get() -> τ_0_1 () #10 0x00000001d2c423ac in SwiftUI.LocationBox.get() -> τ_0_0.Value () #11 0x00000001d2c43c18 in SwiftUI.FlattenedCollectionLocation.get() -> τ_0_0 () #12 0x00000001d2cf2d30 in SwiftUI.Binding.init<τ_0_0 where τ_1_0: Swift.Collection, τ_1_0.Element == SwiftUI.Binding<τ_0_0>>(flattening: τ_1_0) -> SwiftUI.Binding<τ_0_0> () #13 0x00000001d1e6080c in SwiftUI.PickerStyleConfiguration.init(selection: Swift.Array>) -> SwiftUI.PickerStyleConfiguration<τ_0_0> () #14 0x00000001d1ed17b4 in SwiftUI.Picker.body.getter : some () #15 0x00000001d2a51ef0 in function signature specialization of SwiftUI.ViewBodyAccessor.updateBody(of: τ_0_0, changed: Swift.Bool) -> () () #16 0x00000001d2b37818 in closure #1 () -> () in SwiftUI.StaticBody.updateValue() -> () () #17 0x00000001d2b37404 in SwiftUI.StaticBody.updateValue() -> () () #18 0x00000001d2bc4010 in partial apply forwarder for implicit closure #1 (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 () -> (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 (Swift.UnsafePointer<τ_1_0>) -> AttributeGraph.Attribute<τ_0_0> in AttributeGraph.Attribute.init<τ_0_0 where τ_0_0 == τ_1_0.Value, τ_1_0: AttributeGraph.StatefulRule>(τ_1_0) -> AttributeGraph.Attribute<τ_0_0> () #19 0x00000001bdf35f7c in AG::Graph::UpdateStack::update () #20 0x00000001bdf367cc in AG::Graph::update_attribute () #21 0x00000001bdf3e438 in AG::Graph::input_value_ref_slow () #22 0x00000001bdf53f9c in AGGraphGetValue () #23 0x00000001d1e608b4 in SwiftUI._PickerValue.Init1.base.getter : SwiftUI.ResolvedPicker<τ_0_1> () #24 0x00000001d1e609a8 in SwiftUI._PickerValue.Init1.value.getter : SwiftUI._PickerValue<τ_0_0, τ_0_1> () #25 0x00000001d2bc0cd0 in implicit closure #1 (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 () -> (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 (Swift.UnsafePointer<τ_1_0>) -> AttributeGraph.Attribute<τ_0_0> in AttributeGraph.Attribute.init<τ_0_0 where τ_0_0 == τ_1_0.Value, τ_1_0: AttributeGraph.Rule>(τ_1_0) -> AttributeGraph.Attribute<τ_0_0> () #26 0x00000001bdf35f7c in AG::Graph::UpdateStack::update () #27 0x00000001bdf367cc in AG::Graph::update_attribute () #28 0x00000001bdf3e438 in AG::Graph::input_value_ref_slow () #29 0x00000001bdf53f9c in AGGraphGetValue () #30 0x00000001d1d4e46c in SwiftUI.DefaultPickerStyle.Body.base.getter : SwiftUI._PickerValue () #31 0x00000001d1d4e95c in SwiftUI.DefaultPickerStyle.Body.value.getter : some () #32 0x00000001d2bc0cd0 in implicit closure #1 (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 () -> (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 (Swift.UnsafePointer<τ_1_0>) -> AttributeGraph.Attribute<τ_0_0> in AttributeGraph.Attribute.init<τ_0_0 where τ_0_0 == τ_1_0.Value, τ_1_0: AttributeGraph.Rule>(τ_1_0) -> AttributeGraph.Attribute<τ_0_0> () #33 0x00000001bdf35f7c in AG::Graph::UpdateStack::update () #34 0x00000001bdf367cc in AG::Graph::update_attribute () #35 0x00000001bdf3e438 in AG::Graph::input_value_ref_slow () #36 0x00000001bdf53f9c in AGGraphGetValue () #37 0x00000001d1e60b64 in SwiftUI._PickerValue.Init2.value.getter : SwiftUI._PickerValue<τ_0_0, τ_0_1> () #38 0x00000001d2bc0cd0 in implicit closure #1 (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 () -> (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 (Swift.UnsafePointer<τ_1_0>) -> AttributeGraph.Attribute<τ_0_0> in AttributeGraph.Attribute.init<τ_0_0 where τ_0_0 == τ_1_0.Value, τ_1_0: AttributeGraph.Rule>(τ_1_0) -> AttributeGraph.Attribute<τ_0_0> () #39 0x00000001bdf35f7c in AG::Graph::UpdateStack::update () #40 0x00000001bdf367cc in AG::Graph::update_attribute () #41 0x00000001bdf3e438 in AG::Graph::input_value_ref_slow () #42 0x00000001bdf53f9c in AGGraphGetValue () #43 0x00000001d242b148 in SwiftUI.MenuPickerStyle.Body.content.getter : some () #44 0x00000001d242ab74 in SwiftUI.MenuPickerStyle.Body.menu.getter : some () #45 0x00000001d242a9d4 in closure #1 () -> <>.0 in SwiftUI.MenuPickerStyle.Body.value.getter : some () #46 0x00000001d18bec48 in SwiftUI.StaticIf< where τ_0_1: SwiftUI.View, τ_0_2: SwiftUI.View>.init<τ_0_0 where τ_0_0 == SwiftUI.StyleContextAcceptsPredicate<τ_1_0>, τ_1_0: SwiftUI.StyleContext>(in: τ_1_0, then: () -> τ_0_1, else: () -> τ_0_2) -> SwiftUI.StaticIf, τ_0_1, τ_0_2> () #47 0x00000001d242a720 in SwiftUI.MenuPickerStyle.Body.value.getter : some () #48 0x00000001d2bc0cd0 in implicit closure #1 (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 () -> (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 (Swift.UnsafePointer<τ_1_0>) -> AttributeGraph.Attribute<τ_0_0> in AttributeGraph.Attribute.init<τ_0_0 where τ_0_0 == τ_1_0.Value, τ_1_0: AttributeGraph.Rule>(τ_1_0) -> AttributeGraph.Attribute<τ_0_0> () #49 0x00000001bdf35f7c in AG::Graph::UpdateStack::update () #50 0x00000001bdf367cc in AG::Graph::update_attribute () #51 0x00000001bdf3e438 in AG::Graph::input_value_ref_slow () #52 0x00000001bdf53f9c in AGGraphGetValue () #53 0x00000001d2b37244 in SwiftUI.StaticBody.container.getter : τ_0_0.Container () #54 0x00000001d2b377ec in closure #1 () -> () in SwiftUI.StaticBody.updateValue() -> () () #55 0x00000001d2b37404 in SwiftUI.StaticBody.updateValue() -> () () #56 0x00000001d2bc4010 in partial apply forwarder for implicit closure #1 (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 () -> (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 (Swift.UnsafePointer<τ_1_0>) -> AttributeGraph.Attribute<τ_0_0> in AttributeGraph.Attribute.init<τ_0_0 where τ_0_0 == τ_1_0.Value, τ_1_0: AttributeGraph.StatefulRule>(τ_1_0) -> AttributeGraph.Attribute<τ_0_0> () #57 0x00000001bdf35f7c in AG::Graph::UpdateStack::update () #58 0x00000001bdf367cc in AG::Graph::update_attribute () #59 0x00000001bdf3e438 in AG::Graph::input_value_ref_slow () #60 0x00000001bdf53f9c in AGGraphGetValue () #61 0x00000001d2dd9348 in SwiftUI.DynamicViewList.updateValue() -> () () #62 0x00000001d2bc4010 in partial apply forwarder for implicit closure #1 (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 () -> (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 (Swift.UnsafePointer<τ_1_0>) -> AttributeGraph.Attribute<τ_0_0> in AttributeGraph.Attribute.init<τ_0_0 where τ_0_0 == τ_1_0.Value, τ_1_0: AttributeGraph.StatefulRule>(τ_1_0) -> AttributeGraph.Attribute<τ_0_0> () #63 0x00000001bdf35f7c in AG::Graph::UpdateStack::update () #64 0x00000001bdf367cc in AG::Graph::update_attribute () #65 0x00000001bdf3e438 in AG::Graph::input_value_ref_slow () #66 0x00000001bdf53f9c in AGGraphGetValue () #67 0x00000001d2bd8ef0 in SwiftUI.DynamicLayoutViewAdaptor.updatedItems() -> Swift.Optional () #68 0x00000001d287a29c in generic specialization of SwiftUI.DynamicContainerInfo.updateItems(disableTransitions: Swift.Bool) -> (changed: Swift.Bool, hasDepth: Swift.Bool) () #69 0x00000001d2879370 in generic specialization of SwiftUI.DynamicContainerInfo.updateValue() -> () () #70 0x00000001d28983ec in generic specialization > of implicit closure #1 (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 () -> (Swift.UnsafeMutableRawPointer, __C.AGAttribute) -> () in closure #1 (Swift.UnsafePointer<τ_1_0>) -> AttributeGraph.Attribute<τ_0_0> in AttributeGraph.Attribute.init<τ_0_0 where τ_0_0 == τ_1_0.Value, τ_1_0: AttributeGraph.StatefulRule>(τ_1_0) -> AttributeGraph.Attribute<τ_0_0> () #71 0x00000001bdf35f7c in AG::Graph::UpdateStack::update () #72 0x00000001bdf367cc in AG::Graph::update_attribute () #73 0x00000001bdf44068 in AG::Subgraph::update () #74 0x00000001d2fc51d8 in SwiftUI.ViewGraph.updateOutputs(async: Swift.Bool) -> () () #75 0x00000001d2fbb8a8 in closure #2 () -> () in closure #1 () -> () in SwiftUI.ViewRendererHost.render(interval: Swift.Double, updateDisplayList: Swift.Bool, targetTimestamp: Swift.Optional) -> () () #76 0x00000001d2fbb6c0 in closure #1 () -> () in SwiftUI.ViewRendererHost.render(interval: Swift.Double, updateDisplayList: Swift.Bool, targetTimestamp: Swift.Optional) -> () () #77 0x00000001d2fb951c in SwiftUI.ViewRendererHost.render(interval: Swift.Double, updateDisplayList: Swift.Bool, targetTimestamp: Swift.Optional) -> () () #78 0x00000001d23548c0 in SwiftUI._UIHostingView.layoutSubviews() -> () () #79 0x00000001d2354924 in @objc SwiftUI._UIHostingView.layoutSubviews() -> () () #80 0x000000018601c0c4 in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] () #81 0x000000018b06ceb0 in CA::Layer::layout_if_needed () #82 0x000000018b077c34 in CA::Layer::layout_and_display_if_needed () #83 0x000000018afacc58 in CA::Context::commit_transaction () #84 0x000000018afdb468 in CA::Transaction::commit () #85 0x0000000185abb7b4 in __34-[UIApplication _firstCommitBlock]_block_invoke_2 () #86 0x000000018041b0ec in __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ () #87 0x000000018041a824 in __CFRunLoopDoBlocks () #88 0x00000001804150c8 in __CFRunLoopRun () #89 0x0000000180414960 in CFRunLoopRunSpecific () #90 0x0000000190183b10 in GSEventRunModal () #91 0x0000000185aa2b40 in -[UIApplication _run] () #92 0x0000000185aa6d38 in UIApplicationMain () #93 0x00000001d1e2eab4 in closure #1 (Swift.UnsafeMutablePointer>>) -> Swift.Never in SwiftUI.KitRendererCommon(Swift.AnyObject.Type) -> Swift.Never () #94 0x00000001d1e2e7dc in SwiftUI.runApp<τ_0_0 where τ_0_0: SwiftUI.App>(τ_0_0) -> Swift.Never () #95 0x00000001d1b70c8c in static SwiftUI.App.main() -> () () #96 0x0000000103582a88 in static ExampleApp.$main() () #97 0x0000000103582b38 in main at /Users/awatt/Development/swift-perception/Example/Example/ExampleApp.swift:4 ```

Checklist

Expected behavior

No warning for this case.

Actual behavior

Spurious runtime warnings.

Steps to reproduce

I was able to reproduce with this view dropped into the Perception example app. The warning is triggered on first render.

@Perceptible
class OptionModel {
    var option: Int = 0
}

struct OptionView: View {
    @Perception.Bindable var model: OptionModel

    var body: some View {
        WithPerceptionTracking {
            Picker("Option", selection: $model.option) {
                Text("A").tag(0)
                Text("B").tag(1)
            }
        }
    }
}

Perception version information

1.3.5

Destination operating system

iOS 18.0

Xcode version information

Version 16.0 (16A242d)

Swift Compiler version information

swift-driver version: 1.115 Apple Swift version 6.0 (swiftlang-6.0.0.9.10 clang-1600.0.26.2)
Target: arm64-apple-macosx14.0
chwo commented 1 day ago

Perceptible state was accessed but is not being tracked. Track changes to state by wrapping your view in a 'WithPerceptionTracking' view. This must also be done for any escaping, trailing closures, such as 'GeometryReader', LazyVStack (and all lazy views), navigation APIs ('sheet', 'popover', 'fullScreenCover', etc.), and others.

I am experiencing the same issue in the context of a TCA feature. The warning is raised by the selection binding of the Picker. When building with Xcode 15.4.0 there is no runtime waring, but when building with Xcode 16.0.0 there is a runtime warning. Wrapping the Picker in WithPerceptionTracking does not solve the warning.

@Reducer
struct ContentFeature: Reducer {
    @ObservableState
    struct State: Equatable {
        let indices = [0, 1, 2]
        var index: Int = 0
    }

    enum Action {
        case indexChanged(Int)
    }

    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .indexChanged(let index):
                state.index = index
                return .none
            }
        }
    }
}

struct ContentView: View {
    @Perception.Bindable var store: StoreOf<ContentFeature>

    var body: some View {
        List {
            Label("Hello World", systemImage: "globe")

            WithPerceptionTracking {
                Picker("Picker", selection: $store.index.sending(\.indexChanged)) {
                    WithPerceptionTracking {
                        ForEach(store.indices, id: \.self) { index in
                            Text("Number \(index)")
                                .tag(index)
                        }
                    }
                }
            }
        }
    }
}

PickerPerceptionTrackingSample.zip

Technical information

vladyslavsosiuk commented 1 day ago

We are encountering the same issue. For now we managed to workaround the warning with a Picker wrapper type based on @KaiOelfke solution from a slack thread:

public struct _Picker<Label, SelectionValue, Content>: View
where Label: View, SelectionValue: Hashable, Content: View {
    let label: Label
    let content: Content
    let selection: Binding<SelectionValue>

    public init(
        _ titleKey: LocalizedStringKey,
        selection: Binding<SelectionValue>,
        @ViewBuilder content: () -> Content
    ) where Label == Text {
        self.label = Text(titleKey)
        self.content = content()
        self.selection = selection
    }

    public var body: some View {
        _PerceptionLocals.$skipPerceptionChecking.withValue(true) {
            Picker(selection: selection, content: { content }, label: { label })
        }
    }
}