feat: Add AnchoredPopup support for absolute positioning#198
Open
videni wants to merge 15 commits into
Open
Conversation
- Add PopupAlignment.anchored case - Create AnchoredPopup protocol and AnchoredPopupConfig - Implement present(anchoredTo:) API for specifying anchor frame - Create PopupAnchoredStackView for rendering anchored popups - Support originAnchor/popupAnchor configuration for flexible positioning - Add PopupAnchorPoint enum with 9 anchor positions
- Add isTapOutsidePassThroughEnabled config option - Update hitTest for iOS 17/18/26 to support pass-through
92f858a to
d5474d2
Compare
added 5 commits
December 17, 2025 10:53
…mID support - Use single UIHostingController for all popups instead of one per popup - Add AnchoredPopupModel to manage popups, sizes, and frames - Use SwiftUI ZStack + offset() for positioning - Add sizeReader to auto-reposition on size changes - Fix hitTest to check actual popup frames - Fix handleAnchoredPopupHitTest to check anchored popups existence first - Add customID parameter to present(anchoredTo:) for same-type popup differentiation
…rough enabled
- Add hasNonAnchoredPopups() to check for BottomPopup/CenterPopup presence
- When AnchoredPopup has tapOutsidePassThrough enabled:
- If no other popups exist: pass through to app (original behavior)
- If other popups exist: continue hitTest to let them handle touches
- Fixes issue where BottomPopup was unresponsive when AnchoredPopup was displayed
When closing a popup and immediately opening another, priority update was delayed by Animation.duration, causing overlay (fixed zIndex=1) to appear above the new popup content. Changed overlay zIndex from fixed `1` to dynamic `(values.min() ?? 0) - 1`, ensuring overlay always stays below all popup stacks regardless of timing.
Add ability to dismiss all popups except those with specified IDs. - Add removeAllPopupsExcluding case to StackOperation - Add removedAllPopupsExcluding implementation in PopupStack - Add public API dismissAllPopups(excluding:) in PopupStack and View extension This allows keeping specific popups open while closing others.
- Change anchorFrame from static CGRect to closure for dynamic positioning
- Popup position now updates when source view moves (e.g., window resize)
- Use AnchorFrameProvider wrapper for @unchecked Sendable compatibility
- Add boundary avoidance to keep popups within screen bounds
- New edgePadding config (default 16pt)
- New constrainedEdges config (.horizontal by default, .vertical optional)
- Automatically adjust position when popup would overflow edges
- Simplify PopupAnchoredStackView architecture
- Replace stored popupFrames with computed frame(for:) method
- Fix "Publishing changes from within view updates" warnings
- Use @mainactor for proper concurrency isolation
API change: present(anchoredTo:) now takes a closure instead of CGRect
Before: .present(anchoredTo: buttonFrame)
After: .present(anchoredTo: { buttonFrame })
b1f9adc to
62b6af8
Compare
added 8 commits
December 17, 2025 19:53
…oning - Add AnchorRegistry for global frame storage - Add .trackAnchor() modifier to track view frames - Add present(anchoredTo: String) for registry-based positioning - Add present(anchoredTo: CGRect) for static frame positioning - Separate anchorID (positioning) from customID (dismiss management) - Auto-cleanup registry on view disappear
- hitTest now forwards to hostingController instead of returning self - SwiftUI overlay receives events when passThrough=false - Overlay checks dismiss config before closing popup - Events properly pass through when passThrough=true Fixes: clicking outside AnchoredPopup no longer affects parent popups
- Add Public+PopupContainerSize.swift with popupContainerSize Environment key - Inject containerSize into popup content environment in PopupContentView - Enables downstream popups to respond to window size changes (e.g. iPad split screen)
- Remove .background() and .mask() processing in PopupContentView - Remove .cornerRadius() and .backgroundColor() config methods for AnchoredPopup - Keep .overlayColor() for overlay customization AnchoredPopup now delegates all styling (background, corner radius, shadow) to the content view, avoiding conflicts with custom effects like glassEffect.
AnchoredPopup's tap-outside handling is already controlled at UIKit level via AnchoredPopupsContainer.hitTest. The SwiftUI overlay was unnecessary.
- Add allowsHitTesting to overlay based on tapOutsidePassThrough config - Add tapOutsidePassThrough computed property to read last popup's config
…ideBehavior enum - Add TapOutsideBehavior enum with .none, .dismiss, .passThrough cases - Remove isTapOutsidePassThroughEnabled from LocalConfigAnchored - Remove tapOutsideToDismissPopup from AnchoredPopup config - Update PopupView, PopupAnchoredStackView, SceneDelegate to use new enum
07446f6 to
65e42f0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add AnchoredPopup protocol and configuration for absolute positioning of popups, allowing popups to be anchored to specific screen coordinates.
Closes #197
Changes
PopupAlignment.anchoredcaseAnchoredPopupprotocol andAnchoredPopupConfigpresent(anchoredTo:)API for specifying anchor framePopupAnchoredStackViewfor rendering anchored popupsoriginAnchor/popupAnchorconfiguration for flexible positioningPopupAnchorPointenum with 9 anchor positionsUsage
AnchoredPopup Compatibility Limitations
AnchoredPopup is iOS/iPadOS only. Not available on macOS, tvOS, or watchOS.
Why This Limitation Exists
Core Reason: Touch Pass-Through Requires UIKit
AnchoredPopup's key feature is allowing touches outside the popup to pass through to underlying views. This requires:
Why Pure SwiftUI Won't Work
Popup Layer (Popup Window)
↓ tap outside
Main View Layer (App Window) ← needs to receive event
SwiftUI's overlay / ZStack cannot:
UIKit's hitTest override enables precise per-point event routing.
Design Trade-offs
UIKit implementation was chosen for:
Future Options