@@ -17,11 +17,23 @@ class AnchoredPopupsContainer: UIView {
1717
1818 private var hostingController : UIHostingController < AnyView > ?
1919 private var popupModel = AnchoredPopupModel ( )
20+ private var lastBoundsSize : CGSize = . zero
21+
22+ override func layoutSubviews( ) {
23+ super. layoutSubviews ( )
24+ guard bounds. size != lastBoundsSize else { return }
25+ lastBoundsSize = bounds. size
26+
27+ DispatchQueue . main. async { [ weak self] in
28+ guard let self else { return }
29+ self . popupModel. containerSize = self . bounds. size
30+ }
31+ }
2032
2133 /// Returns true only if touch is inside a popup frame
2234 override func point( inside point: CGPoint , with event: UIEvent ? ) -> Bool {
23- for (_ , frame ) in popupModel. popupFrames {
24- if frame. contains ( point) {
35+ for popup in popupModel. popups {
36+ if let frame = popupModel . frame ( for : popup ) , frame. contains ( point) {
2537 return true
2638 }
2739 }
@@ -30,30 +42,17 @@ class AnchoredPopupsContainer: UIView {
3042
3143 /// Returns hit view only if touch is inside a popup frame, otherwise passes through
3244 override func hitTest( _ point: CGPoint , with event: UIEvent ? ) -> UIView ? {
33- for (_, frame) in popupModel. popupFrames {
34- if frame. contains ( point) {
35- // Touch inside popup, let UIHostingController handle it
45+ for popup in popupModel. popups {
46+ if let frame = popupModel. frame ( for: popup) , frame. contains ( point) {
3647 return hostingController? . view. hitTest ( point, with: event)
3748 }
3849 }
39- // Touch outside all popups, pass through
4050 return nil
4151 }
4252
4353 /// Updates popups in the container
4454 func updatePopups( _ popups: [ AnyPopup ] , viewModel: VM . AnchoredStack ) {
45- // Clean up frames for removed popups
46- let currentIds = Set ( popups. map { $0. id. rawValue } )
47- let existingIds = Set ( popupModel. popupFrames. keys)
48- for id in existingIds. subtracting ( currentIds) {
49- popupModel. popupFrames. removeValue ( forKey: id)
50- popupModel. popupSizes. removeValue ( forKey: id)
51- }
52-
53- popupModel. popups = popups
54- popupModel. viewModel = viewModel
55-
56- // Create hosting controller if needed
55+ // Create hosting controller if needed (sync, only once)
5756 if hostingController == nil {
5857 let containerView = AnchoredPopupContainerView ( model: popupModel)
5958 let hc = UIHostingController ( rootView: AnyView ( containerView) )
@@ -63,6 +62,20 @@ class AnchoredPopupsContainer: UIView {
6362 addSubview ( hc. view)
6463 hostingController = hc
6564 }
65+
66+ DispatchQueue . main. async { [ weak self] in
67+ guard let self else { return }
68+
69+ // Clean up sizes for removed popups
70+ let currentIds = Set ( popups. map { $0. id. rawValue } )
71+ let existingIds = Set ( self . popupModel. popupSizes. keys)
72+ for id in existingIds. subtracting ( currentIds) {
73+ self . popupModel. popupSizes. removeValue ( forKey: id)
74+ }
75+
76+ self . popupModel. popups = popups
77+ self . popupModel. viewModel = viewModel
78+ }
6679 }
6780
6881 /// Installs container directly on Window (above rootViewController.view)
@@ -87,11 +100,26 @@ class AnchoredPopupsContainer: UIView {
87100
88101// MARK: - Popup Model (ObservableObject for SwiftUI)
89102
103+ @MainActor
90104private class AnchoredPopupModel : ObservableObject {
91105 @Published var popups : [ AnyPopup ] = [ ]
92106 @Published var popupSizes : [ String : CGSize ] = [ : ]
93- @Published var popupFrames : [ String : CGRect ] = [ : ] // Store frame for hitTest
107+ @Published var containerSize : CGSize = . zero
94108 var viewModel : VM . AnchoredStack ?
109+
110+ /// Calculate frame for popup (called during render, not stored)
111+ func frame( for popup: AnyPopup ) -> CGRect ? {
112+ let popupId = popup. id. rawValue
113+ guard let size = popupSizes [ popupId] ,
114+ let viewModel = viewModel else { return nil }
115+
116+ let position = viewModel. calculatePopupPosition (
117+ for: popup,
118+ popupSize: size,
119+ containerSize: containerSize
120+ )
121+ return CGRect ( origin: position, size: size)
122+ }
95123}
96124
97125// MARK: - SwiftUI Container View
@@ -103,41 +131,21 @@ private struct AnchoredPopupContainerView: View {
103131 ZStack ( alignment: . topLeading) {
104132 ForEach ( model. popups, id: \. self) { popup in
105133 let popupId = popup. id. rawValue
106- let hasSize = model. popupSizes [ popupId ] != nil
134+ let frame = model. frame ( for : popup )
107135
108136 PopupContentView ( popup: popup, viewModel: model. viewModel)
109- . opacity ( hasSize ? 1 : 0 )
137+ . opacity ( frame != nil ? 1 : 0 )
110138 . sizeReader { size in
111139 if model. popupSizes [ popupId] != size {
112140 model. popupSizes [ popupId] = size
113141 }
114142 }
115- . offset ( popupOffset ( for : popup ) )
143+ . offset ( x : frame ? . origin . x ?? 0 , y : frame ? . origin . y ?? 0 )
116144 }
117145 }
118146 . frame ( maxWidth: . infinity, maxHeight: . infinity, alignment: . topLeading)
119147 . edgesIgnoringSafeArea ( . all)
120148 }
121-
122- /// Calculate offset for popup positioning and store frame for hitTest
123- private func popupOffset( for popup: AnyPopup ) -> CGSize {
124- let popupId = popup. id. rawValue
125- guard let size = model. popupSizes [ popupId] , let viewModel = model. viewModel else {
126- return . zero
127- }
128-
129- let position = viewModel. calculatePopupPosition ( for: popup, popupSize: size)
130-
131- // Store frame for hitTest
132- let frame = CGRect ( origin: position, size: size)
133- if model. popupFrames [ popupId] != frame {
134- DispatchQueue . main. async {
135- self . model. popupFrames [ popupId] = frame
136- }
137- }
138-
139- return CGSize ( width: position. x, height: position. y)
140- }
141149}
142150
143151/// SwiftUI content for a single popup
0 commit comments