├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ └── MenuWithAView.xcscheme ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Frameworks └── ContextMenuAccessoryStructs.xcframework │ ├── Info.plist │ ├── ios-arm64 │ └── ContextMenuAccessoryStructs.framework │ │ ├── ContextMenuAccessoryStructs │ │ ├── Headers │ │ └── ContextMenuAccessoryStructs.h │ │ ├── Info.plist │ │ ├── Modules │ │ └── module.modulemap │ │ └── _CodeSignature │ │ └── CodeResources │ └── ios-arm64_x86_64-simulator │ └── ContextMenuAccessoryStructs.framework │ ├── ContextMenuAccessoryStructs │ ├── Headers │ └── ContextMenuAccessoryStructs.h │ ├── Info.plist │ ├── Modules │ └── module.modulemap │ └── _CodeSignature │ └── CodeResources ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── MenuWithAView │ ├── AccessoryItem.swift │ ├── ContextMenuIdentifierView.swift │ ├── Example.swift │ └── Extensions │ ├── Array │ └── Array+Compact.swift │ ├── ContextMenuAccessoryAnchor │ └── ContextMenuAccessoryAnchor+InitWithAttachmentAlignment.swift │ ├── UIContextMenuInteraction │ ├── UIContextMenuInteraction+AccessoryViewWithConfiguration.swift │ └── UIContextMenuInteraction+Swizzle.swift │ ├── UIView │ └── UIView+FirstSubviewOfType.swift │ └── View │ └── View+ContextMenuAccessories.swift └── assets ├── example1.gif └── icon.png /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Test Swift Package on iOS 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | name: Test Swift 6.0 on iOS (${{ matrix.config }}) 12 | runs-on: macos-latest # iOS testing requires macOS 13 | 14 | strategy: 15 | matrix: 16 | config: [debug, release] 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Select Xcode (latest) 23 | uses: maxim-lobanov/setup-xcode@v1 24 | with: 25 | xcode-version: 'latest' 26 | 27 | - name: Setup Swift 6.0 28 | uses: SwiftyLab/setup-swift@latest 29 | with: 30 | swift-version: '6.0' # Use quotes to ensure correct version parsing 31 | 32 | - name: Check Swift version 33 | run: swift --version 34 | 35 | - name: Find an iOS Simulator 36 | id: find_simulator 37 | run: | 38 | # Find an available iOS simulator runtime and device 39 | RUNTIME_ID=$(xcrun simctl list runtimes ios --json | jq -r '.runtimes | map(select(.isAvailable)) | sort_by(.version) | last.identifier') 40 | if [ -z "$RUNTIME_ID" ] || [ "$RUNTIME_ID" == "null" ]; then 41 | echo "::error::No iOS runtime found." 42 | exit 1 43 | fi 44 | DEVICE_ID=$(xcrun simctl list devices --json | jq -r --arg RT_ID "$RUNTIME_ID" '.devices[$RT_ID] | map(select(.isAvailable)) | .[0].udid') 45 | if [ -z "$DEVICE_ID" ] || [ "$DEVICE_ID" == "null" ]; then 46 | echo "::error::No available iOS simulator device found for runtime $RUNTIME_ID." 47 | exit 1 48 | fi 49 | echo "Found iOS Simulator Runtime: $RUNTIME_ID" 50 | echo "Found iOS Simulator Device UDID: $DEVICE_ID" 51 | echo "SIMULATOR_DESTINATION=platform=iOS Simulator,id=$DEVICE_ID" >> $GITHUB_OUTPUT 52 | 53 | - name: Build and Test (${{ matrix.config }}) 54 | run: | 55 | echo "Using simulator destination: ${{ steps.find_simulator.outputs.SIMULATOR_DESTINATION }}" 56 | xcodebuild build -scheme MenuWithAView -sdk $(xcrun --sdk iphonesimulator --show-sdk-path) -destination "${{ steps.find_simulator.outputs.SIMULATOR_DESTINATION }}" SWIFT_VERSION=6.0 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | # .swiftpm 31 | 32 | .build/ 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | # 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 64 | .swiftpm/xcode/.DS_Store 65 | .swiftpm/.DS_Store 66 | .DS_Store 67 | .DS_Store 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/MenuWithAView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | git@aethers.world. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to MenuWithAView 2 | 3 | First off, thank you for your interest in contributing to MenuWithAView 🙏 Your help makes MenuWithAView better for everyone. 4 | 5 | ## Code of Conduct 6 | 7 | Please read and follow the [Code of Conduct](./CODE_OF_CONDUCT.md). We're committed to fostering an open, welcoming, and respectful community. 8 | 9 | ## How to Report Issues 10 | 11 | Before opening a new issue, search existing issues to avoid duplicates. When filing a bug report, please include: 12 | - MenuWithAView version (e.g. `0.1.2`) and your Swift/Xcode versions 13 | - Target platform (iOS 15.0+), device or simulator 14 | - A concise description of the problem and steps to reproduce 15 | - Minimal code snippet or sample project demonstrating the issue 16 | - Any relevant console logs or screenshots 17 | 18 | ## How to Propose Features 19 | 20 | If you have an idea for a new feature or enhancement: 21 | 1. Search existing feature requests or discuss in Slack/Discord. 22 | 2. Open a new “Feature Request” issue with: 23 | - A clear problem statement 24 | - Proposed API or usage sketch 25 | - Any design or UX considerations 26 | 27 | ## Getting Started 28 | 29 | 1. Fork the repo and clone locally: 30 | ```bash 31 | git clone https://github.com/aeastr/MenuWithAView.git 32 | cd MenuWithAView 33 | ``` 34 | 2. Create a feature branch: 35 | ```bash 36 | git checkout -b feat/my-new-feature 37 | ``` 38 | 3. Make your changes. 39 | 40 | 4. Commit with a clear message: 41 | ``` 42 | feat: add template options 43 | ``` 44 | 45 | 5. Push and open a Pull Request against `main`. 46 | 47 | ## Coding Guidelines 48 | 49 | - Use idiomatic Swift & SwiftUI conventions 50 | - Structure code for readability and reuse 51 | - Keep public APIs minimal and well-documented 52 | - If you introduce new API, add samples 53 | - Format your code with `swift-format` or Xcode’s built-in formatter 54 | - Write unit tests _where applicable_ 55 | 56 | ## Documentation 57 | 58 | - Update installation, usage, and examples sections as needed 59 | - Add or update screenshots/GIFs under `docs/images` with descriptive filenames 60 | 61 | ## Pull Request Process 62 | 63 | 1. Link your PR to the relevant issue (if there is one) 64 | 2. Describe what you’ve changed and why 65 | 3. Keep PRs focused—one feature or fix per PR 66 | 4. Ensure all examples build and run without warnings or errors 67 | 5. Be responsive to review feedback 68 | 69 | ## Continuous Integration 70 | 71 | All PRs are validated by CI: 72 | - Build on latest Xcode 73 | - Run example targets 74 | - Lint documentation links 75 | 76 | Please address any CI failures before merging. 77 | 78 | ## License 79 | 80 | By contributing, you agree that your contributions will be licensed under the MIT License. 81 | 82 | --- 83 | 84 | Thank you for helping make MenuWithAView even more magical! 🚀 85 | -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AvailableLibraries 6 | 7 | 8 | BinaryPath 9 | ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs 10 | LibraryIdentifier 11 | ios-arm64_x86_64-simulator 12 | LibraryPath 13 | ContextMenuAccessoryStructs.framework 14 | SupportedArchitectures 15 | 16 | arm64 17 | x86_64 18 | 19 | SupportedPlatform 20 | ios 21 | SupportedPlatformVariant 22 | simulator 23 | 24 | 25 | BinaryPath 26 | ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs 27 | LibraryIdentifier 28 | ios-arm64 29 | LibraryPath 30 | ContextMenuAccessoryStructs.framework 31 | SupportedArchitectures 32 | 33 | arm64 34 | 35 | SupportedPlatform 36 | ios 37 | 38 | 39 | CFBundlePackageType 40 | XFWK 41 | XCFrameworkFormatVersion 42 | 1.0 43 | 44 | 45 | -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeastr/MenuWithAView/8fafbbaba91626c68ad4fc4d6a6bdcded9462d72/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Headers/ContextMenuAccessoryStructs.h: -------------------------------------------------------------------------------- 1 | // 2 | // ContextMenuAccessoryStructs.h 3 | // ContextMenuAccessoryStructs 4 | // 5 | // Created by Seb Vidal on 11/05/2025. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for ContextMenuAccessoryStructs. 11 | FOUNDATION_EXPORT double ContextMenuAccessoryStructsVersionNumber; 12 | 13 | //! Project version string for ContextMenuAccessoryStructs. 14 | FOUNDATION_EXPORT const unsigned char ContextMenuAccessoryStructsVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | typedef struct { 19 | unsigned long long attachment; 20 | unsigned long long alignment; 21 | double attachmentOffset; 22 | double alignmentOffset; 23 | long long gravity; 24 | } ContextMenuAccessoryAnchor; 25 | -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeastr/MenuWithAView/8fafbbaba91626c68ad4fc4d6a6bdcded9462d72/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Info.plist -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module ContextMenuAccessoryStructs { 2 | umbrella header "ContextMenuAccessoryStructs.h" 3 | export * 4 | 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Headers/ContextMenuAccessoryStructs.h 8 | 9 | b9Luviw1b76Lr6p1xgNrWNfPCVY= 10 | 11 | Info.plist 12 | 13 | oS7eZhHij+Sz+QXDnS0j7qeyoFY= 14 | 15 | Modules/module.modulemap 16 | 17 | ATR3fNklhb4n/v9ZT1/V52kwESk= 18 | 19 | 20 | files2 21 | 22 | Headers/ContextMenuAccessoryStructs.h 23 | 24 | hash2 25 | 26 | SwK0arudKlqsKFRArld22fmqwIqpiFxdeNrWJ36DmvQ= 27 | 28 | 29 | Modules/module.modulemap 30 | 31 | hash2 32 | 33 | bNjsWBrAeGn0pRiBgSwZN5WWpE7sPRHaQT1gVEZp5Eg= 34 | 35 | 36 | 37 | rules 38 | 39 | ^.* 40 | 41 | ^.*\.lproj/ 42 | 43 | optional 44 | 45 | weight 46 | 1000 47 | 48 | ^.*\.lproj/locversion.plist$ 49 | 50 | omit 51 | 52 | weight 53 | 1100 54 | 55 | ^Base\.lproj/ 56 | 57 | weight 58 | 1010 59 | 60 | ^version.plist$ 61 | 62 | 63 | rules2 64 | 65 | .*\.dSYM($|/) 66 | 67 | weight 68 | 11 69 | 70 | ^(.*/)?\.DS_Store$ 71 | 72 | omit 73 | 74 | weight 75 | 2000 76 | 77 | ^.* 78 | 79 | ^.*\.lproj/ 80 | 81 | optional 82 | 83 | weight 84 | 1000 85 | 86 | ^.*\.lproj/locversion.plist$ 87 | 88 | omit 89 | 90 | weight 91 | 1100 92 | 93 | ^Base\.lproj/ 94 | 95 | weight 96 | 1010 97 | 98 | ^Info\.plist$ 99 | 100 | omit 101 | 102 | weight 103 | 20 104 | 105 | ^PkgInfo$ 106 | 107 | omit 108 | 109 | weight 110 | 20 111 | 112 | ^embedded\.provisionprofile$ 113 | 114 | weight 115 | 20 116 | 117 | ^version\.plist$ 118 | 119 | weight 120 | 20 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeastr/MenuWithAView/8fafbbaba91626c68ad4fc4d6a6bdcded9462d72/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Headers/ContextMenuAccessoryStructs.h: -------------------------------------------------------------------------------- 1 | // 2 | // ContextMenuAccessoryStructs.h 3 | // ContextMenuAccessoryStructs 4 | // 5 | // Created by Seb Vidal on 11/05/2025. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for ContextMenuAccessoryStructs. 11 | FOUNDATION_EXPORT double ContextMenuAccessoryStructsVersionNumber; 12 | 13 | //! Project version string for ContextMenuAccessoryStructs. 14 | FOUNDATION_EXPORT const unsigned char ContextMenuAccessoryStructsVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | typedef struct { 19 | unsigned long long attachment; 20 | unsigned long long alignment; 21 | double attachmentOffset; 22 | double alignmentOffset; 23 | long long gravity; 24 | } ContextMenuAccessoryAnchor; 25 | -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeastr/MenuWithAView/8fafbbaba91626c68ad4fc4d6a6bdcded9462d72/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Info.plist -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module ContextMenuAccessoryStructs { 2 | umbrella header "ContextMenuAccessoryStructs.h" 3 | export * 4 | 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Headers/ContextMenuAccessoryStructs.h 8 | 9 | b9Luviw1b76Lr6p1xgNrWNfPCVY= 10 | 11 | Info.plist 12 | 13 | 5DvCNs2Br0B+tFd3LGX2GWX3YI4= 14 | 15 | Modules/module.modulemap 16 | 17 | ATR3fNklhb4n/v9ZT1/V52kwESk= 18 | 19 | 20 | files2 21 | 22 | Headers/ContextMenuAccessoryStructs.h 23 | 24 | hash2 25 | 26 | SwK0arudKlqsKFRArld22fmqwIqpiFxdeNrWJ36DmvQ= 27 | 28 | 29 | Modules/module.modulemap 30 | 31 | hash2 32 | 33 | bNjsWBrAeGn0pRiBgSwZN5WWpE7sPRHaQT1gVEZp5Eg= 34 | 35 | 36 | 37 | rules 38 | 39 | ^.* 40 | 41 | ^.*\.lproj/ 42 | 43 | optional 44 | 45 | weight 46 | 1000 47 | 48 | ^.*\.lproj/locversion.plist$ 49 | 50 | omit 51 | 52 | weight 53 | 1100 54 | 55 | ^Base\.lproj/ 56 | 57 | weight 58 | 1010 59 | 60 | ^version.plist$ 61 | 62 | 63 | rules2 64 | 65 | .*\.dSYM($|/) 66 | 67 | weight 68 | 11 69 | 70 | ^(.*/)?\.DS_Store$ 71 | 72 | omit 73 | 74 | weight 75 | 2000 76 | 77 | ^.* 78 | 79 | ^.*\.lproj/ 80 | 81 | optional 82 | 83 | weight 84 | 1000 85 | 86 | ^.*\.lproj/locversion.plist$ 87 | 88 | omit 89 | 90 | weight 91 | 1100 92 | 93 | ^Base\.lproj/ 94 | 95 | weight 96 | 1010 97 | 98 | ^Info\.plist$ 99 | 100 | omit 101 | 102 | weight 103 | 20 104 | 105 | ^PkgInfo$ 106 | 107 | omit 108 | 109 | weight 110 | 20 111 | 112 | ^embedded\.provisionprofile$ 113 | 114 | weight 115 | 20 116 | 117 | ^version\.plist$ 118 | 119 | weight 120 | 20 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Aether & Seb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MenuWithAView", 8 | platforms: [ 9 | .iOS(.v18) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, making them visible to other packages. 13 | .library( 14 | name: "MenuWithAView", 15 | targets: ["MenuWithAView"] 16 | ), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .target( 22 | name: "MenuWithAView", 23 | dependencies: ["ContextMenuAccessoryStructs"] 24 | ), 25 | .binaryTarget( 26 | name: "ContextMenuAccessoryStructs", 27 | path: "Frameworks/ContextMenuAccessoryStructs.xcframework" 28 | ) 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | MenuWithAView Logo 3 |

MenuWithAView

4 |

5 | MenuWithAView is a SwiftUI package that lets you add accessory views to your context menu interactions, with UIKit's private _UIContextMenuAccessoryView. 6 |
7 | Compatible with iOS 18 and later 8 |

9 |
10 | 11 |
12 | 13 | 14 | Swift Version 15 | 16 | 17 | iOS 18 | 19 | 20 | License: MIT 21 | 22 |
23 | 24 | ## **Demo** 25 | 26 | ![Example](/assets/example1.gif) 27 | 28 | --- 29 | 30 | ## contextMenuAccessory 31 | 32 | `contextMenuAccessory` is a SwiftUI modifier that lets you attach an accessory view to a `.contextMenu`. You can control the accessory’s placement, location, alignment, and tracking axis. 33 | 34 | **DocC documentation is available for this modifier.** 35 | 36 | ### Parameters 37 | 38 | - `placement`: Where the accessory is attached relative to the context menu. 39 | *(Default: `.center`)* 40 | - `location`: The location where the accessory appears. 41 | *(Default: `.preview`)* 42 | - `alignment`: How the accessory aligns within its container. 43 | *(Default: `.leading`)* 44 | - `trackingAxis`: The axis along which the accessory tracks user interaction. 45 | *(Default: `[.xAxis, .yAxis]`)* 46 | - `accessory`: The view to display as the accessory. 47 | 48 | ### Example 49 | 50 | ```swift 51 | Text("Turtle Rock") 52 | .padding() 53 | .contextMenu { 54 | Button(action: {}) { 55 | Label("Button", systemImage: "circle") 56 | } 57 | } 58 | .contextMenuAccessory( 59 | placement: .center, 60 | location: .preview, 61 | alignment: .leading, 62 | trackingAxis: .yAxis 63 | ) { 64 | Text("Accessory View") 65 | .font(.title2) 66 | .padding(8) 67 | .background(Color.blue.opacity(0.6)) 68 | .clipShape(RoundedRectangle(cornerRadius: 12)) 69 | .padding(16) 70 | } 71 | ``` 72 | 73 | --- 74 | 75 | ## **Acknowledgments** 76 | 77 | Special thanks to [@sebjvidal](https://github.com/sebjvidal) for [writing about _UIContextMenuAccessoryView](https://sebvidal.com/blog/accessorise-your-context-menu-interactions/), and helping with the development of this package! 78 | 79 | ## License 80 | 81 | This project is released under the MIT License. See [LICENSE](LICENSE.md) for details. 82 | 83 | ## Contributing 84 | 85 | Contributions are welcome! Please feel free to submit a Pull Request. Before you begin, take a moment to review the [Contributing Guide](CONTRIBUTING.md) for details on issue reporting, coding standards, and the PR process. 86 | 87 | ## Support 88 | 89 | If you like this project, please consider giving it a ⭐️ 90 | 91 | --- 92 | 93 | ## Where to find us 94 | 95 | | | Aether | Seb | 96 | |---------|----------------|------------------| 97 | | Twitter | [@AetherAurelia](https://x.com/AetherAurelia) | [@SebJVidal](https://x.com/SebJVidal) | 98 | | Threads | [@aetheraurelia](https://www.threads.net/@aetheraurelia) | - | 99 | | Bluesky | [aethers.world](https://bsky.app/profile/aethers.world) | - | 100 | | LinkedIn| [aether](https://www.linkedin.com/in/willjones24) | - | 101 | | GitHub | here, obviously | here!! | 102 | | Website | - | [sebvidal.com](https://sebvidal.com/) | 103 | 104 | 105 | --- 106 | 107 |

Built with 🍏🕵️🤝👜 by Aether and Seb

108 | -------------------------------------------------------------------------------- /Sources/MenuWithAView/AccessoryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextMenuAccessory.swift 3 | // MenuWithAView 4 | // 5 | // Created by Aether Aurelia Seb Vidal on 11/05/2025. 6 | // 7 | 8 | import SwiftUI 9 | import ContextMenuAccessoryStructs 10 | 11 | struct AccessoryItem: View { 12 | let configuration: Configuration 13 | let content: () -> Content 14 | 15 | init(configuration: ContextMenuAccessoryConfiguration, content: @escaping () -> Content) { 16 | self.configuration = configuration 17 | self.content = content 18 | } 19 | 20 | init(placement: Placement, content: @escaping () -> Content) { 21 | self.configuration = Configuration(placement: placement) 22 | self.content = content 23 | } 24 | 25 | var body: some View { 26 | content() 27 | } 28 | } 29 | 30 | extension AccessoryItem { 31 | public typealias Location = ContextMenuAccessoryLocation 32 | 33 | public typealias Placement = ContextMenuAccessoryPlacement 34 | 35 | public typealias Alignment = ContextMenuAccessoryAlignment 36 | 37 | public typealias TrackingAxis = ContextMenuAccessoryTrackingAxis 38 | 39 | typealias Configuration = ContextMenuAccessoryConfiguration 40 | } 41 | 42 | public enum ContextMenuAccessoryLocation: Int { 43 | case background = 0 44 | case preview = 1 45 | case menu = 2 46 | } 47 | 48 | public enum ContextMenuAccessoryPlacement: UInt64 { 49 | case top = 1 50 | case leading = 2 51 | case center = 3 52 | case bottom = 4 53 | case trailing = 8 54 | } 55 | 56 | public enum ContextMenuAccessoryAlignment: UInt64 { 57 | case top = 1 58 | case leading = 2 59 | case center = 3 60 | case bottom = 4 61 | case trailing = 8 62 | } 63 | 64 | public struct ContextMenuAccessoryTrackingAxis: OptionSet, Sendable { 65 | public let rawValue: Int 66 | 67 | public init(rawValue: Int) { 68 | self.rawValue = rawValue 69 | } 70 | 71 | public static var xAxis: ContextMenuAccessoryTrackingAxis { 72 | return ContextMenuAccessoryTrackingAxis(rawValue: 1 << 0) 73 | } 74 | 75 | public static var yAxis: ContextMenuAccessoryTrackingAxis { 76 | return ContextMenuAccessoryTrackingAxis(rawValue: 1 << 1) 77 | } 78 | } 79 | 80 | /// Configuration for context menu accessories, including placement, location, alignment, and tracking axis. 81 | struct ContextMenuAccessoryConfiguration: Identifiable { 82 | let id: UUID = UUID() 83 | 84 | var location: ContextMenuAccessoryLocation = .preview 85 | 86 | // controls the attachment point 87 | var placement: ContextMenuAccessoryPlacement = .center 88 | 89 | // controls alignment to attachment 90 | var alignment: ContextMenuAccessoryAlignment = .leading 91 | 92 | var trackingAxis: ContextMenuAccessoryTrackingAxis = [.xAxis, .yAxis] 93 | 94 | var anchor: ContextMenuAccessoryAnchor { 95 | return ContextMenuAccessoryAnchor(attachment: placement.rawValue, alignment: alignment.rawValue) 96 | } 97 | } 98 | 99 | #Preview { 100 | MenuWithAView_Example() 101 | } 102 | -------------------------------------------------------------------------------- /Sources/MenuWithAView/ContextMenuIdentifierView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextMenuIdentifierView.swift 3 | // MenuWithAView 4 | // 5 | // Created by Aether Aurelia and Seb Vidal on 11/05/2025. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | import ContextMenuAccessoryStructs 11 | 12 | struct ContextMenuIdentifierView: UIViewRepresentable { 13 | let accessoryView: () -> AccessoryItem 14 | 15 | func makeUIView(context: Context) -> some UIView { 16 | let rootView = accessoryView() 17 | let hostingView = _UIHostingView(rootView: rootView) 18 | let identifierView = ContextMenuIdentifierUIView(accessoryView: hostingView, configuration: rootView.configuration) 19 | 20 | return identifierView 21 | } 22 | 23 | func updateUIView(_ uiView: UIViewType, context: Context) {} 24 | } 25 | 26 | class ContextMenuIdentifierUIView: UIView { 27 | let accessoryView: UIView 28 | let configuration: ContextMenuAccessoryConfiguration 29 | 30 | init(accessoryView: UIView, configuration: ContextMenuAccessoryConfiguration) { 31 | self.accessoryView = accessoryView 32 | self.configuration = configuration 33 | 34 | super.init(frame: .zero) 35 | 36 | UIContextMenuInteraction.swizzle_delegate_getAccessoryViewsForConfigurationIfNeeded() 37 | } 38 | 39 | required init?(coder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | } 43 | 44 | #Preview { 45 | MenuWithAView_Example() 46 | } 47 | -------------------------------------------------------------------------------- /Sources/MenuWithAView/Example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Example.swift 3 | // MenuWithAView 4 | // 5 | // Created by Aether on 11/05/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct MenuWithAView_Example: View { 11 | 12 | @State var placement: ContextMenuAccessoryPlacement = .top 13 | @State var alignment: ContextMenuAccessoryAlignment = .center 14 | @State var location: ContextMenuAccessoryLocation = .preview 15 | 16 | private let blueGradient = [ 17 | Color(red: 0.2, green: 0.36, blue: 0.90), 18 | Color(red: 0.15, green: 0.45, blue: 0.85) 19 | ] 20 | 21 | // Human-readable names 22 | 23 | private var placementName: String { 24 | switch placement { 25 | case .top: return "top" 26 | case .bottom: return "bottom" 27 | case .center: return "center" 28 | case .leading: return "leading" 29 | case .trailing: return "trailing" 30 | } 31 | } 32 | 33 | private var alignmentName: String { 34 | switch alignment { 35 | case .top: return "top" 36 | case .bottom: return "bottom" 37 | case .center: return "center" 38 | case .leading: return "leading" 39 | case .trailing: return "trailing" 40 | } 41 | } 42 | 43 | private var locationName: String { 44 | switch location { 45 | case .background: return "background" 46 | case .menu: return "menu" 47 | case .preview: return "preview" 48 | } 49 | } 50 | 51 | public init(){} 52 | 53 | public var body: some View { 54 | NavigationStack { 55 | ScrollView { 56 | VStack(alignment: .leading, spacing: 34) { 57 | // Description 58 | Text("Attach a view to your context menu with flexible placement, location, alignment, and tracking. Use the controls below and long-press the demo box to see it in action.") 59 | .font(.subheadline) 60 | .foregroundColor(.secondary) 61 | .padding(.horizontal) 62 | .multilineTextAlignment(.center) 63 | 64 | // Demo Box 65 | RoundedRectangle(cornerRadius: 16) 66 | .fill( 67 | LinearGradient( 68 | gradient: Gradient(colors: blueGradient), 69 | startPoint: .topLeading, 70 | endPoint: .bottomTrailing 71 | ) 72 | ) 73 | .frame(width: 200, height: 120) 74 | .shadow(color: Color(red: 0.15, green: 0.45, blue: 0.85).opacity(0.25), radius: 10, y: 1) 75 | .contentShape([.contextMenuPreview, .interaction], RoundedRectangle(cornerRadius: 16)) 76 | .contextMenu { 77 | Button { } label: { Label("Button", systemImage: "circle") } 78 | } 79 | .contextMenuAccessory( 80 | placement: placement, 81 | location: location, 82 | alignment: alignment, 83 | trackingAxis: .yAxis 84 | ) { 85 | Text("Accessory View") 86 | .font(.title2) 87 | .padding(8) 88 | .background(Color.blue.opacity(0.6)) 89 | .clipShape(RoundedRectangle(cornerRadius: 12)) 90 | .padding(16) 91 | } 92 | .frame(maxWidth: .infinity) 93 | .padding(.bottom, 10) 94 | 95 | HStack{ 96 | VStack(alignment: .center, spacing: 8) { 97 | Text("Placement") 98 | .font(.headline) 99 | Picker("Placement", selection: $placement) { 100 | Text("Top").tag(ContextMenuAccessoryPlacement.top) 101 | Text("Bottom").tag(ContextMenuAccessoryPlacement.bottom) 102 | Text("Center").tag(ContextMenuAccessoryPlacement.center) 103 | Text("Leading").tag(ContextMenuAccessoryPlacement.leading) 104 | Text("Trailing").tag(ContextMenuAccessoryPlacement.trailing) 105 | } 106 | .pickerStyle(.menu) 107 | } 108 | .frame(maxWidth: .infinity) 109 | 110 | VStack(alignment: .center, spacing: 8) { 111 | Text("Alignment") 112 | .font(.headline) 113 | Picker("Alignment", selection: $alignment) { 114 | Text("Top").tag(ContextMenuAccessoryAlignment.top) 115 | Text("Bottom").tag(ContextMenuAccessoryAlignment.bottom) 116 | Text("Center").tag(ContextMenuAccessoryAlignment.center) 117 | Text("Leading").tag(ContextMenuAccessoryAlignment.leading) 118 | Text("Trailing").tag(ContextMenuAccessoryAlignment.trailing) 119 | } 120 | .pickerStyle(.menu) 121 | } 122 | .frame(maxWidth: .infinity) 123 | 124 | VStack(alignment: .center, spacing: 8) { 125 | Text("Location") 126 | .font(.headline) 127 | Picker("Location", selection: $location) { 128 | Text("Background").tag(ContextMenuAccessoryLocation.background) 129 | Text("Menu").tag(ContextMenuAccessoryLocation.menu) 130 | Text("Preview").tag(ContextMenuAccessoryLocation.preview) 131 | } 132 | .pickerStyle(.menu) 133 | } 134 | .frame(maxWidth: .infinity) 135 | } 136 | 137 | // Code Example 138 | VStack(alignment: .leading, spacing: 8) { 139 | Text("Example Usage") 140 | .font(.headline) 141 | Text( 142 | """ 143 | .contextMenu { 144 | ... 145 | } 146 | .contextMenuAccessory( 147 | placement: .\(placementName), 148 | location: .\(locationName), 149 | alignment: .\(alignmentName), 150 | trackingAxis: .yAxis 151 | ) { 152 | Text("Accessory View") 153 | } 154 | """ 155 | ) 156 | .font(.system(.body, design: .monospaced)) 157 | .frame(maxWidth: .infinity, alignment: .leading) 158 | .padding() 159 | .background(Color(.systemBackground)) 160 | .clipShape(RoundedRectangle(cornerRadius: 16)) 161 | } 162 | 163 | } 164 | .padding() 165 | } 166 | .background(Color(UIColor.systemGroupedBackground)) 167 | .navigationTitle("MenuWithAView") 168 | .navigationBarTitleDisplayMode(.inline) 169 | } 170 | } 171 | } 172 | 173 | #Preview { 174 | MenuWithAView_Example() 175 | } 176 | -------------------------------------------------------------------------------- /Sources/MenuWithAView/Extensions/Array/Array+Compact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // MenuWithAView 4 | // 5 | // Created by Aether Aurelia and Seb Vidal on 11/05/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array { 11 | func compact() -> [T] where Element == Optional { 12 | compactMap { $0 } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/MenuWithAView/Extensions/ContextMenuAccessoryAnchor/ContextMenuAccessoryAnchor+InitWithAttachmentAlignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextMenuAccessoryAnchor+InitWithAttachmentAlignment.swift 3 | // MenuWithAView 4 | // 5 | // Created by Aether Aurelia and Seb Vidal on 11/05/2025. 6 | // 7 | 8 | import ContextMenuAccessoryStructs 9 | 10 | extension ContextMenuAccessoryAnchor { 11 | init(attachment: UInt64, alignment: UInt64) { 12 | self.init(attachment: attachment, alignment: alignment, attachmentOffset: 0, alignmentOffset: 0, gravity: 0) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIContextMenuInteraction+AccessoryViewWithConfiguration.swift 3 | // MenuWithAView 4 | // 5 | // Created by Aether Aurelia and Seb Vidal on 11/05/2025. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | import ContextMenuAccessoryStructs 11 | 12 | extension UIContextMenuInteraction { 13 | static func accessoryView(configuration: AccessoryItem.Configuration) -> UIView? { 14 | let accessoryViewClassString = ["View", "Accessory", "Menu", "Context", "UI", "_"].reversed().joined() 15 | let accessoryViewClass = NSClassFromString(accessoryViewClassString) as? UIView.Type 16 | 17 | guard let accessoryViewClass else { return nil } 18 | 19 | let accessoryView = accessoryViewClass.init() 20 | 21 | let locationString = [":", "Location", "set"].reversed().joined() 22 | let locationSelector = NSSelectorFromString(locationString) 23 | 24 | if accessoryView.responds(to: locationSelector) { 25 | accessoryView.setValue(configuration.location.rawValue, forKey: "location") 26 | } 27 | 28 | let trackingAxisString = [":", "Axis", "Tracking", "set"].reversed().joined() 29 | let trackingAxisSelector = NSSelectorFromString(trackingAxisString) 30 | 31 | if accessoryView.responds(to: trackingAxisSelector) { 32 | let key = ["Axis", "tracking"].reversed().joined() 33 | accessoryView.setValue(configuration.trackingAxis.rawValue, forKey: key) 34 | } 35 | 36 | let anchorString = [":", "Anchor", "set"].reversed().joined() 37 | let anchorSelector = NSSelectorFromString(anchorString) 38 | 39 | if accessoryView.responds(to: anchorSelector) { 40 | let method = class_getInstanceMethod(accessoryViewClass, anchorSelector)! 41 | let implementation = method_getImplementation(method) 42 | 43 | let type = (@convention(c) (AnyObject, Selector, ContextMenuAccessoryAnchor) -> Void).self 44 | let setAnchor = unsafeBitCast(implementation, to: type) 45 | setAnchor(accessoryView, anchorSelector, configuration.anchor) 46 | } 47 | 48 | return accessoryView 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIContextMenuInteraction+Swizzle.swift 3 | // MenuWithAView 4 | // 5 | // Created by Aether Aurelia and Seb Vidal on 11/05/2025. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | extension UIContextMenuInteraction { 12 | private static var needsSwizzle_delegate_getAccessoryViewsForConfiguration: Bool = true 13 | 14 | static func swizzle_delegate_getAccessoryViewsForConfigurationIfNeeded() { 15 | guard needsSwizzle_delegate_getAccessoryViewsForConfiguration else { return } 16 | 17 | let originalString = [":", "Configuration", "For", "Views", "Accessory", "get", "_", "delegate", "_"].reversed().joined() 18 | let swizzledString = [":", "Configuration", "For", "Views", "Accessory", "get", "_", "delegate", "_", "swizzled"].reversed().joined() 19 | 20 | let originalSelector = NSSelectorFromString(originalString) 21 | let swizzledSelector = NSSelectorFromString(swizzledString) 22 | 23 | guard instancesRespond(to: originalSelector), instancesRespond(to: swizzledSelector) else { return } 24 | 25 | let originalMethod = class_getInstanceMethod(UIContextMenuInteraction.self, originalSelector) 26 | let swizzledMethod = class_getInstanceMethod(UIContextMenuInteraction.self, swizzledSelector) 27 | 28 | guard let originalMethod, let swizzledMethod else { return } 29 | 30 | method_exchangeImplementations(originalMethod, swizzledMethod) 31 | 32 | needsSwizzle_delegate_getAccessoryViewsForConfiguration = false 33 | } 34 | 35 | @objc dynamic func swizzled_delegate_getAccessoryViewsForConfiguration(_ configuration: UIContextMenuConfiguration) -> [UIView] { 36 | if let identifierView = view?.firstSubview(ofType: ContextMenuIdentifierUIView.self) { 37 | 38 | let contentView = identifierView.accessoryView 39 | contentView.frame.size = contentView.intrinsicContentSize 40 | 41 | let accessoryView = UIContextMenuInteraction.accessoryView(configuration: identifierView.configuration) 42 | accessoryView?.frame.size = contentView.intrinsicContentSize 43 | accessoryView?.addSubview(contentView) 44 | 45 | return [accessoryView].compact() 46 | } else { 47 | return swizzled_delegate_getAccessoryViewsForConfiguration(configuration) 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Sources/MenuWithAView/Extensions/UIView/UIView+FirstSubviewOfType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+FirstSubviewOfType.swift 3 | // MenuWithAView 4 | // 5 | // Created by Ather Aurelia and Seb Vidal on 11/05/2025. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | func firstSubview(ofType type: T.Type) -> T? { 12 | for subview in subviews { 13 | if let candidate = subview as? T { 14 | return candidate 15 | } else if let candidate = subview.firstSubview(ofType: type) { 16 | return candidate 17 | } 18 | } 19 | 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+ContextMenuAccessories.swift 3 | // MenuWithAView 4 | // 5 | // Created by Aether Aurelia and Seb Vidal on 11/05/2025. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | import ContextMenuAccessoryStructs 11 | 12 | private struct AccessoryWrapper: View { 13 | let configuration: ContextMenuAccessoryConfiguration 14 | let accessory: () -> AccessoryView 15 | 16 | var body: some View { 17 | ContextMenuIdentifierView(accessoryView: { 18 | AccessoryItem(configuration: configuration) { 19 | accessory() 20 | } 21 | }) 22 | } 23 | } 24 | 25 | public extension View { 26 | /// Adds an accessory view to instances of `.contextMenu`. 27 | /// 28 | /// > Note: This modifier should be used in combination with `.contextMenu`. 29 | /// 30 | /// - Parameters: 31 | /// - placement: The placement of the accessory relative to the context menu. *(Optional, default: `.center`)* 32 | /// - location: The location where the accessory should appear. *(Optional, default: `.preview`)* 33 | /// - alignment: The alignment of the accessory within its container. *(Optional, default: `.leading`)* 34 | /// - trackingAxis: The axis along which the accessory tracks user interaction. *(Optional, default: `[.xAxis, .yAxis]`)* 35 | /// - accessory: A view builder that creates the accessory view. 36 | /// 37 | /// For more details on default values, see ``ContextMenuAccessoryConfiguration``. 38 | /// 39 | /// Example usage: 40 | /// 41 | /// ```swift 42 | /// Text("Turtle Rock") 43 | /// .padding() 44 | /// .contextMenu { 45 | /// Button(action: {}) { 46 | /// Label("Button", systemImage: "circle") 47 | /// } 48 | /// } 49 | /// .contextMenuAccessory( 50 | /// placement: placement, 51 | /// location: location, 52 | /// alignment: alignment, 53 | /// trackingAxis: .yAxis 54 | /// ) { 55 | /// Text("Accessory View") 56 | /// .font(.title2) 57 | /// .padding(8) 58 | /// .background(Color.blue.opacity(0.6)) 59 | /// .clipShape(RoundedRectangle(cornerRadius: 12)) 60 | /// .padding(16) 61 | /// } 62 | /// ``` 63 | func contextMenuAccessory( 64 | placement: ContextMenuAccessoryPlacement? = nil, 65 | location: ContextMenuAccessoryLocation? = nil, 66 | alignment: ContextMenuAccessoryAlignment? = nil, 67 | trackingAxis: ContextMenuAccessoryTrackingAxis? = nil, 68 | @ViewBuilder accessory: @escaping () -> AccessoryView 69 | ) -> some View { 70 | var config = ContextMenuAccessoryConfiguration() 71 | if let placement = placement { config.placement = placement } 72 | if let location = location { config.location = location } 73 | if let alignment = alignment { config.alignment = alignment } 74 | if let trackingAxis = trackingAxis { config.trackingAxis = trackingAxis } 75 | 76 | let wrapped = background { 77 | AccessoryWrapper(configuration: config, accessory: accessory) 78 | .accessibilityHidden(true) 79 | } 80 | return wrapped.id(config.id) 81 | } 82 | } 83 | 84 | #Preview { 85 | MenuWithAView_Example() 86 | } 87 | -------------------------------------------------------------------------------- /assets/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeastr/MenuWithAView/8fafbbaba91626c68ad4fc4d6a6bdcded9462d72/assets/example1.gif -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeastr/MenuWithAView/8fafbbaba91626c68ad4fc4d6a6bdcded9462d72/assets/icon.png --------------------------------------------------------------------------------