11import SwiftUI
2+ import UIKit
23
34@available ( iOS 16 . 0 , * )
45struct SwiftUIDisclosureGroup : View {
@@ -7,14 +8,188 @@ struct SwiftUIDisclosureGroup: View {
78
89 var body : some View {
910 List {
10- DisclosureGroup ( " Expanded Section " , isExpanded: $isExpanded) {
11- Text ( " Item 1 " )
12- Text ( " Item 2 " )
11+ // MARK: - Normal DisclosureGroups (SwiftUI)
12+
13+ Section ( " SwiftUI DisclosureGroup " ) {
14+ DisclosureGroup ( " Expanded Section " , isExpanded: $isExpanded) {
15+ Text ( " Item 1 " )
16+ Text ( " Item 2 " )
17+ }
18+
19+ DisclosureGroup ( " Collapsed Section " , isExpanded: $isCollapsed) {
20+ Text ( " Hidden Item " )
21+ }
1322 }
1423
15- DisclosureGroup ( " Collapsed Section " , isExpanded: $isCollapsed) {
16- Text ( " Hidden Item " )
24+ // MARK: - Interactive mutation tests
25+
26+ if #available( iOS 18 . 0 , * ) {
27+ Section ( " Interactive Mutation Tests " ) {
28+ MutationTestRow ( ) . frame ( height: 300 )
29+ }
1730 }
1831 }
1932 }
2033}
34+
35+ // MARK: - Interactive mutation test view (no override — reads Apple's real behavior)
36+
37+ @available ( iOS 18 . 0 , * )
38+ private struct MutationTestRow : UIViewRepresentable {
39+ func makeUIView( context: Context ) -> MutationTestContainerView {
40+ return MutationTestContainerView ( )
41+ }
42+
43+ func updateUIView( _ uiView: MutationTestContainerView , context: Context ) { }
44+ }
45+
46+ @available ( iOS 18 . 0 , * )
47+ private class MutationTestContainerView : UIView {
48+ // Plain UIView — NO override of _accessibilityExpandedStatus
49+ private let testView = UIView ( )
50+ private let logLabel = UILabel ( )
51+ private var logLines : [ String ] = [ ]
52+
53+ private let privateGetSel = NSSelectorFromString ( " _accessibilityExpandedStatus " )
54+ private let privateSetSel = NSSelectorFromString ( " _setAccessibilityExpandedStatus: " )
55+
56+ override init ( frame: CGRect ) {
57+ super. init ( frame: frame)
58+ setup ( )
59+ }
60+
61+ @available ( * , unavailable)
62+ required init ? ( coder: NSCoder ) { fatalError ( ) }
63+
64+ private func readPrivate( ) -> Int {
65+ guard testView. responds ( to: privateGetSel) else { return - 1 }
66+ let imp = testView. method ( for: privateGetSel)
67+ typealias Fn = @convention ( c) ( AnyObject , Selector ) -> Int
68+ let fn = unsafeBitCast ( imp, to: Fn . self)
69+ return fn ( testView, privateGetSel)
70+ }
71+
72+ private func writePrivate( _ value: Int ) -> Bool {
73+ guard testView. responds ( to: privateSetSel) else { return false }
74+ let imp = testView. method ( for: privateSetSel)
75+ typealias Fn = @convention ( c) ( AnyObject , Selector , Int ) -> Void
76+ let fn = unsafeBitCast ( imp, to: Fn . self)
77+ fn ( testView, privateSetSel, value)
78+ return true
79+ }
80+
81+ private func readPublic( ) -> Int {
82+ return testView. accessibilityExpandedStatus. rawValue
83+ }
84+
85+ private func log( _ msg: String ) {
86+ logLines. append ( msg)
87+ logLabel. text = logLines. joined ( separator: " \n " )
88+ }
89+
90+ private func logState( _ prefix: String ) {
91+ log ( " \( prefix) → pub= \( readPublic ( ) ) priv= \( readPrivate ( ) ) " )
92+ }
93+
94+ private func setup( ) {
95+ testView. isAccessibilityElement = true
96+ testView. accessibilityLabel = " Test Target "
97+
98+ // Check if private setter exists
99+ let hasPrivateSetter = testView. responds ( to: privateSetSel)
100+
101+ logLabel. numberOfLines = 0
102+ logLabel. font = . monospacedSystemFont( ofSize: 11 , weight: . regular)
103+ logLabel. translatesAutoresizingMaskIntoConstraints = false
104+ addSubview ( logLabel)
105+ NSLayoutConstraint . activate ( [
106+ logLabel. topAnchor. constraint ( equalTo: topAnchor, constant: 8 ) ,
107+ logLabel. leadingAnchor. constraint ( equalTo: leadingAnchor, constant: 8 ) ,
108+ logLabel. trailingAnchor. constraint ( equalTo: trailingAnchor, constant: - 8 ) ,
109+ ] )
110+
111+ let stack = UIStackView ( )
112+ stack. axis = . vertical
113+ stack. spacing = 4
114+ stack. translatesAutoresizingMaskIntoConstraints = false
115+ addSubview ( stack)
116+ NSLayoutConstraint . activate ( [
117+ stack. bottomAnchor. constraint ( equalTo: bottomAnchor, constant: - 8 ) ,
118+ stack. leadingAnchor. constraint ( equalTo: leadingAnchor, constant: 8 ) ,
119+ stack. trailingAnchor. constraint ( equalTo: trailingAnchor, constant: - 8 ) ,
120+ ] )
121+
122+ func btn( _ title: String , _ action: @escaping ( ) -> Void ) -> UIButton {
123+ let b = UIButton ( type: . system)
124+ b. setTitle ( title, for: . normal)
125+ b. titleLabel? . font = . systemFont( ofSize: 12 )
126+ b. addAction ( UIAction { _ in action ( ) } , for: . touchUpInside)
127+ return b
128+ }
129+
130+ // Row 1: Public setter
131+ let row1 = UIStackView ( arrangedSubviews: [
132+ btn ( " pub=expanded " ) { [ weak self] in
133+ self ? . testView. accessibilityExpandedStatus = . expanded
134+ self ? . logState ( " pub=expanded " )
135+ } ,
136+ btn ( " pub=collapsed " ) { [ weak self] in
137+ self ? . testView. accessibilityExpandedStatus = . collapsed
138+ self ? . logState ( " pub=collapsed " )
139+ } ,
140+ btn ( " pub=unsupported " ) { [ weak self] in
141+ self ? . testView. accessibilityExpandedStatus = . unsupported
142+ self ? . logState ( " pub=unsupported " )
143+ } ,
144+ ] )
145+ row1. distribution = . fillEqually
146+
147+ // Row 2: Private setter (if it exists)
148+ let row2 = UIStackView ( arrangedSubviews: [
149+ btn ( " _priv=1(exp) " ) { [ weak self] in
150+ guard let self else { return }
151+ if self . writePrivate ( 1 ) {
152+ self . logState ( " _priv=1 " )
153+ } else {
154+ self . log ( " _setAccessibilityExpandedStatus: NOT FOUND " )
155+ }
156+ } ,
157+ btn ( " _priv=2(col) " ) { [ weak self] in
158+ guard let self else { return }
159+ if self . writePrivate ( 2 ) {
160+ self . logState ( " _priv=2 " )
161+ } else {
162+ self . log ( " _setAccessibilityExpandedStatus: NOT FOUND " )
163+ }
164+ } ,
165+ btn ( " _priv=0(unsup) " ) { [ weak self] in
166+ guard let self else { return }
167+ if self . writePrivate ( 0 ) {
168+ self . logState ( " _priv=0 " )
169+ } else {
170+ self . log ( " _setAccessibilityExpandedStatus: NOT FOUND " )
171+ }
172+ } ,
173+ ] )
174+ row2. distribution = . fillEqually
175+
176+ // Row 3: Read / Clear
177+ let row3 = UIStackView ( arrangedSubviews: [
178+ btn ( " Read State " ) { [ weak self] in
179+ self ? . logState ( " Read " )
180+ } ,
181+ btn ( " Clear Log " ) { [ weak self] in
182+ self ? . logLines = [ ]
183+ self ? . logLabel. text = " "
184+ } ,
185+ ] )
186+ row3. distribution = . fillEqually
187+
188+ stack. addArrangedSubview ( row1)
189+ stack. addArrangedSubview ( row2)
190+ stack. addArrangedSubview ( row3)
191+
192+ log ( " _setAccessibilityExpandedStatus exists: \( hasPrivateSetter) " )
193+ logState ( " Initial " )
194+ }
195+ }
0 commit comments