├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Examples └── TriggerKitDemo │ ├── TriggerKitDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── TriggerKitDemo │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── appstore1024.png │ │ ├── ipad152.png │ │ ├── ipad76.png │ │ ├── ipadNotification20.png │ │ ├── ipadNotification40.png │ │ ├── ipadPro167.png │ │ ├── ipadSettings29.png │ │ ├── ipadSettings58.png │ │ ├── ipadSpotlight40.png │ │ ├── ipadSpotlight80.png │ │ ├── iphone120.png │ │ ├── iphone180.png │ │ ├── mac1024.png │ │ ├── mac128.png │ │ ├── mac16.png │ │ ├── mac256.png │ │ ├── mac32.png │ │ ├── mac512.png │ │ ├── mac64.png │ │ ├── notification40.png │ │ ├── notification60.png │ │ ├── settings58.png │ │ ├── settings87.png │ │ ├── spotlight120.png │ │ └── spotlight80.png │ ├── Contents.json │ └── logo.bluetooth.symbolset │ │ ├── Contents.json │ │ └── logo.bluetooth.svg │ ├── Info.plist │ ├── Model │ └── AppActionsViewModel.swift │ ├── Modifiers │ └── PressActions.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── TriggerKitDemoApp.swift │ └── Views │ ├── ActionsView.swift │ ├── AppTabView.swift │ └── DemoView.swift ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── TriggerKit │ ├── Model │ ├── TKAppActionConstraints.swift │ ├── TKEvent.swift │ ├── TKPayLoad.swift │ ├── TKTriggerMidiCC.swift │ └── TKTriggerMidiNote.swift │ ├── TKBus.swift │ └── Views │ └── TKBluetoothMIDIView.swift ├── Tests └── TriggerKitTests │ ├── ConfigTests.swift │ ├── MappingTests.swift │ └── TestConfig.swift └── media └── logo.svg /.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 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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, caste, color, religion, or sexual 10 | identity 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 overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | 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 address, 35 | 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 | https://github.com/kkostov or https://social.iamkonstantin.eu/web/@hbko. 64 | 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [mozilla coc]: https://github.com/mozilla/diversity 132 | [faq]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How you can Contribute 2 | 3 | ## Answer issues and contribute to discussions 4 | 5 | Answering [issues](https://github.com/lightbeamapps/TriggerKit/issues), participating in [discussions](https://github.com/lightbeamapps/TriggerKit/discussions) is a great way to help, get familiar with the library, and shape its direction. 6 | 7 | ## Contribute to the TriggerKit codebase 8 | 9 | ### Clone the `main` branch on your machine. 10 | 11 | - Open the folder in Xcode (or your preferred editor with Swift support) 12 | 13 | ### Run tests 14 | 15 | You can run tests using the Swift CLI by running `swift test` in the root of the project. 16 | 17 | You can also execute tests in Xcode by switching to the Test navigator and executing one or more tests. 18 | 19 | ### Please respect the existing coding style 20 | 21 | - Get familiar with the [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/). 22 | - Whitespace-only lines are not trimmed. 23 | - We use SwiftLint to ensure a consistent look and feel of the library code. Your changes should contain no SwiftLint errors or warnings. Please run and check SwiftLint on any code contributions before submitting e.g. `swiftlint lint --fix --format`. 24 | - Avoid bringing in new libraries or dependencies without good justification. Any PR that brings in a new library needs to make the case for why it is necessary. 25 | 26 | ### Please provide documentation for your changes 27 | 28 | All methods and types that the library makes public, should have a meaningful description and information on how to use. 29 | 30 | It is recommended to include unit tests covering your changes. 31 | 32 | Optionally, you may consider extending one of the examples in order to showcase the new functionality. 33 | 34 | ### Talk to the maintainer 🤙 35 | 36 | We'd love it if you'd talk to us over on the Fediverse! The current maintainer and admin for TriggerKit is: 37 | 38 | - [David Gary Wood](https://social.davidgarywood.com/@davidgarywood) 39 | 40 | ### Open a pull request with your changes (targeting the `main` branch)! 41 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 553AAC8B2A09D8D500CA76CB /* PressActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553AAC8A2A09D8D500CA76CB /* PressActions.swift */; }; 11 | 558463BA2A0274EF00646730 /* AppActionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558463B92A0274EF00646730 /* AppActionsViewModel.swift */; }; 12 | 558463BD2A02750800646730 /* TriggerKit in Frameworks */ = {isa = PBXBuildFile; productRef = 558463BC2A02750800646730 /* TriggerKit */; }; 13 | 558463BF2A02846D00646730 /* ActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558463BE2A02846D00646730 /* ActionsView.swift */; }; 14 | 558463C32A02873300646730 /* DemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558463C22A02873300646730 /* DemoView.swift */; }; 15 | 55ABA28E2A0251E0004A84E5 /* TriggerKitDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55ABA28D2A0251E0004A84E5 /* TriggerKitDemoApp.swift */; }; 16 | 55ABA2902A0251E0004A84E5 /* AppTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55ABA28F2A0251E0004A84E5 /* AppTabView.swift */; }; 17 | 55ABA2922A0251E1004A84E5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 55ABA2912A0251E1004A84E5 /* Assets.xcassets */; }; 18 | 55ABA2952A0251E1004A84E5 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 55ABA2942A0251E1004A84E5 /* Preview Assets.xcassets */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 553AAC8A2A09D8D500CA76CB /* PressActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressActions.swift; sourceTree = ""; }; 23 | 558463B82A02524700646730 /* TriggerKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TriggerKit; path = ../..; sourceTree = ""; }; 24 | 558463B92A0274EF00646730 /* AppActionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActionsViewModel.swift; sourceTree = ""; }; 25 | 558463BE2A02846D00646730 /* ActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionsView.swift; sourceTree = ""; }; 26 | 558463C22A02873300646730 /* DemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoView.swift; sourceTree = ""; }; 27 | 55ABA28A2A0251E0004A84E5 /* TriggerKitDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TriggerKitDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 55ABA28D2A0251E0004A84E5 /* TriggerKitDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerKitDemoApp.swift; sourceTree = ""; }; 29 | 55ABA28F2A0251E0004A84E5 /* AppTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTabView.swift; sourceTree = ""; }; 30 | 55ABA2912A0251E1004A84E5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 31 | 55ABA2942A0251E1004A84E5 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 32 | 55F0076A2A062A0700D7D2AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | 55ABA2872A0251E0004A84E5 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | 558463BD2A02750800646730 /* TriggerKit in Frameworks */, 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | 553AAC8C2A09D8E000CA76CB /* Modifiers */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | 553AAC8A2A09D8D500CA76CB /* PressActions.swift */, 51 | ); 52 | path = Modifiers; 53 | sourceTree = ""; 54 | }; 55 | 558463B72A02524700646730 /* Packages */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 558463B82A02524700646730 /* TriggerKit */, 59 | ); 60 | name = Packages; 61 | sourceTree = ""; 62 | }; 63 | 558463BB2A02750800646730 /* Frameworks */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | ); 67 | name = Frameworks; 68 | sourceTree = ""; 69 | }; 70 | 558463C02A0284C800646730 /* Views */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 55ABA28F2A0251E0004A84E5 /* AppTabView.swift */, 74 | 558463C22A02873300646730 /* DemoView.swift */, 75 | 558463BE2A02846D00646730 /* ActionsView.swift */, 76 | ); 77 | path = Views; 78 | sourceTree = ""; 79 | }; 80 | 558463C12A0284D000646730 /* Model */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 558463B92A0274EF00646730 /* AppActionsViewModel.swift */, 84 | ); 85 | path = Model; 86 | sourceTree = ""; 87 | }; 88 | 55ABA2812A0251E0004A84E5 = { 89 | isa = PBXGroup; 90 | children = ( 91 | 558463B72A02524700646730 /* Packages */, 92 | 55ABA28C2A0251E0004A84E5 /* TriggerKitDemo */, 93 | 55ABA28B2A0251E0004A84E5 /* Products */, 94 | 558463BB2A02750800646730 /* Frameworks */, 95 | ); 96 | sourceTree = ""; 97 | }; 98 | 55ABA28B2A0251E0004A84E5 /* Products */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 55ABA28A2A0251E0004A84E5 /* TriggerKitDemo.app */, 102 | ); 103 | name = Products; 104 | sourceTree = ""; 105 | }; 106 | 55ABA28C2A0251E0004A84E5 /* TriggerKitDemo */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 55F0076A2A062A0700D7D2AE /* Info.plist */, 110 | 55ABA28D2A0251E0004A84E5 /* TriggerKitDemoApp.swift */, 111 | 553AAC8C2A09D8E000CA76CB /* Modifiers */, 112 | 558463C12A0284D000646730 /* Model */, 113 | 558463C02A0284C800646730 /* Views */, 114 | 55ABA2912A0251E1004A84E5 /* Assets.xcassets */, 115 | 55ABA2932A0251E1004A84E5 /* Preview Content */, 116 | ); 117 | path = TriggerKitDemo; 118 | sourceTree = ""; 119 | }; 120 | 55ABA2932A0251E1004A84E5 /* Preview Content */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 55ABA2942A0251E1004A84E5 /* Preview Assets.xcassets */, 124 | ); 125 | path = "Preview Content"; 126 | sourceTree = ""; 127 | }; 128 | /* End PBXGroup section */ 129 | 130 | /* Begin PBXNativeTarget section */ 131 | 55ABA2892A0251E0004A84E5 /* TriggerKitDemo */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = 55ABA2982A0251E1004A84E5 /* Build configuration list for PBXNativeTarget "TriggerKitDemo" */; 134 | buildPhases = ( 135 | 55ABA2862A0251E0004A84E5 /* Sources */, 136 | 55ABA2872A0251E0004A84E5 /* Frameworks */, 137 | 55ABA2882A0251E0004A84E5 /* Resources */, 138 | ); 139 | buildRules = ( 140 | ); 141 | dependencies = ( 142 | ); 143 | name = TriggerKitDemo; 144 | packageProductDependencies = ( 145 | 558463BC2A02750800646730 /* TriggerKit */, 146 | ); 147 | productName = TriggerKitDemo; 148 | productReference = 55ABA28A2A0251E0004A84E5 /* TriggerKitDemo.app */; 149 | productType = "com.apple.product-type.application"; 150 | }; 151 | /* End PBXNativeTarget section */ 152 | 153 | /* Begin PBXProject section */ 154 | 55ABA2822A0251E0004A84E5 /* Project object */ = { 155 | isa = PBXProject; 156 | attributes = { 157 | BuildIndependentTargetsInParallel = 1; 158 | LastSwiftUpdateCheck = 1430; 159 | LastUpgradeCheck = 1430; 160 | TargetAttributes = { 161 | 55ABA2892A0251E0004A84E5 = { 162 | CreatedOnToolsVersion = 14.3; 163 | }; 164 | }; 165 | }; 166 | buildConfigurationList = 55ABA2852A0251E0004A84E5 /* Build configuration list for PBXProject "TriggerKitDemo" */; 167 | compatibilityVersion = "Xcode 14.0"; 168 | developmentRegion = en; 169 | hasScannedForEncodings = 0; 170 | knownRegions = ( 171 | en, 172 | Base, 173 | ); 174 | mainGroup = 55ABA2812A0251E0004A84E5; 175 | productRefGroup = 55ABA28B2A0251E0004A84E5 /* Products */; 176 | projectDirPath = ""; 177 | projectRoot = ""; 178 | targets = ( 179 | 55ABA2892A0251E0004A84E5 /* TriggerKitDemo */, 180 | ); 181 | }; 182 | /* End PBXProject section */ 183 | 184 | /* Begin PBXResourcesBuildPhase section */ 185 | 55ABA2882A0251E0004A84E5 /* Resources */ = { 186 | isa = PBXResourcesBuildPhase; 187 | buildActionMask = 2147483647; 188 | files = ( 189 | 55ABA2952A0251E1004A84E5 /* Preview Assets.xcassets in Resources */, 190 | 55ABA2922A0251E1004A84E5 /* Assets.xcassets in Resources */, 191 | ); 192 | runOnlyForDeploymentPostprocessing = 0; 193 | }; 194 | /* End PBXResourcesBuildPhase section */ 195 | 196 | /* Begin PBXSourcesBuildPhase section */ 197 | 55ABA2862A0251E0004A84E5 /* Sources */ = { 198 | isa = PBXSourcesBuildPhase; 199 | buildActionMask = 2147483647; 200 | files = ( 201 | 55ABA2902A0251E0004A84E5 /* AppTabView.swift in Sources */, 202 | 553AAC8B2A09D8D500CA76CB /* PressActions.swift in Sources */, 203 | 55ABA28E2A0251E0004A84E5 /* TriggerKitDemoApp.swift in Sources */, 204 | 558463BA2A0274EF00646730 /* AppActionsViewModel.swift in Sources */, 205 | 558463C32A02873300646730 /* DemoView.swift in Sources */, 206 | 558463BF2A02846D00646730 /* ActionsView.swift in Sources */, 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | }; 210 | /* End PBXSourcesBuildPhase section */ 211 | 212 | /* Begin XCBuildConfiguration section */ 213 | 55ABA2962A0251E1004A84E5 /* Debug */ = { 214 | isa = XCBuildConfiguration; 215 | buildSettings = { 216 | ALWAYS_SEARCH_USER_PATHS = NO; 217 | CLANG_ANALYZER_NONNULL = YES; 218 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 220 | CLANG_ENABLE_MODULES = YES; 221 | CLANG_ENABLE_OBJC_ARC = YES; 222 | CLANG_ENABLE_OBJC_WEAK = YES; 223 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 224 | CLANG_WARN_BOOL_CONVERSION = YES; 225 | CLANG_WARN_COMMA = YES; 226 | CLANG_WARN_CONSTANT_CONVERSION = YES; 227 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 228 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 229 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 230 | CLANG_WARN_EMPTY_BODY = YES; 231 | CLANG_WARN_ENUM_CONVERSION = YES; 232 | CLANG_WARN_INFINITE_RECURSION = YES; 233 | CLANG_WARN_INT_CONVERSION = YES; 234 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 235 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 236 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 237 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 238 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 239 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 240 | CLANG_WARN_STRICT_PROTOTYPES = YES; 241 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 242 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 243 | CLANG_WARN_UNREACHABLE_CODE = YES; 244 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 245 | COPY_PHASE_STRIP = NO; 246 | DEBUG_INFORMATION_FORMAT = dwarf; 247 | ENABLE_STRICT_OBJC_MSGSEND = YES; 248 | ENABLE_TESTABILITY = YES; 249 | GCC_C_LANGUAGE_STANDARD = gnu11; 250 | GCC_DYNAMIC_NO_PIC = NO; 251 | GCC_NO_COMMON_BLOCKS = YES; 252 | GCC_OPTIMIZATION_LEVEL = 0; 253 | GCC_PREPROCESSOR_DEFINITIONS = ( 254 | "DEBUG=1", 255 | "$(inherited)", 256 | ); 257 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 258 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 259 | GCC_WARN_UNDECLARED_SELECTOR = YES; 260 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 261 | GCC_WARN_UNUSED_FUNCTION = YES; 262 | GCC_WARN_UNUSED_VARIABLE = YES; 263 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 264 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 265 | MTL_FAST_MATH = YES; 266 | ONLY_ACTIVE_ARCH = YES; 267 | SDKROOT = iphoneos; 268 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 269 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 270 | }; 271 | name = Debug; 272 | }; 273 | 55ABA2972A0251E1004A84E5 /* Release */ = { 274 | isa = XCBuildConfiguration; 275 | buildSettings = { 276 | ALWAYS_SEARCH_USER_PATHS = NO; 277 | CLANG_ANALYZER_NONNULL = YES; 278 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 279 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 280 | CLANG_ENABLE_MODULES = YES; 281 | CLANG_ENABLE_OBJC_ARC = YES; 282 | CLANG_ENABLE_OBJC_WEAK = YES; 283 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 284 | CLANG_WARN_BOOL_CONVERSION = YES; 285 | CLANG_WARN_COMMA = YES; 286 | CLANG_WARN_CONSTANT_CONVERSION = YES; 287 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 288 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 289 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 290 | CLANG_WARN_EMPTY_BODY = YES; 291 | CLANG_WARN_ENUM_CONVERSION = YES; 292 | CLANG_WARN_INFINITE_RECURSION = YES; 293 | CLANG_WARN_INT_CONVERSION = YES; 294 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 295 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 296 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 297 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 298 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 299 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 300 | CLANG_WARN_STRICT_PROTOTYPES = YES; 301 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 302 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 303 | CLANG_WARN_UNREACHABLE_CODE = YES; 304 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 305 | COPY_PHASE_STRIP = NO; 306 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 307 | ENABLE_NS_ASSERTIONS = NO; 308 | ENABLE_STRICT_OBJC_MSGSEND = YES; 309 | GCC_C_LANGUAGE_STANDARD = gnu11; 310 | GCC_NO_COMMON_BLOCKS = YES; 311 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 312 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 313 | GCC_WARN_UNDECLARED_SELECTOR = YES; 314 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 315 | GCC_WARN_UNUSED_FUNCTION = YES; 316 | GCC_WARN_UNUSED_VARIABLE = YES; 317 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 318 | MTL_ENABLE_DEBUG_INFO = NO; 319 | MTL_FAST_MATH = YES; 320 | SDKROOT = iphoneos; 321 | SWIFT_COMPILATION_MODE = wholemodule; 322 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 323 | VALIDATE_PRODUCT = YES; 324 | }; 325 | name = Release; 326 | }; 327 | 55ABA2992A0251E1004A84E5 /* Debug */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 331 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 332 | CODE_SIGN_STYLE = Automatic; 333 | CURRENT_PROJECT_VERSION = 1; 334 | DEVELOPMENT_ASSET_PATHS = "\"TriggerKitDemo/Preview Content\""; 335 | DEVELOPMENT_TEAM = AY32NZUWTJ; 336 | ENABLE_PREVIEWS = YES; 337 | GENERATE_INFOPLIST_FILE = YES; 338 | INFOPLIST_FILE = TriggerKitDemo/Info.plist; 339 | INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Connect to bluetooth MIDI devices"; 340 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 341 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 342 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 343 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 344 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 345 | LD_RUNPATH_SEARCH_PATHS = ( 346 | "$(inherited)", 347 | "@executable_path/Frameworks", 348 | ); 349 | MARKETING_VERSION = 1.0; 350 | PRODUCT_BUNDLE_IDENTIFIER = com.lightbeamapps.TriggerKitDemo; 351 | PRODUCT_NAME = "$(TARGET_NAME)"; 352 | SWIFT_EMIT_LOC_STRINGS = YES; 353 | SWIFT_VERSION = 5.0; 354 | TARGETED_DEVICE_FAMILY = "1,2"; 355 | }; 356 | name = Debug; 357 | }; 358 | 55ABA29A2A0251E1004A84E5 /* Release */ = { 359 | isa = XCBuildConfiguration; 360 | buildSettings = { 361 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 362 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 363 | CODE_SIGN_STYLE = Automatic; 364 | CURRENT_PROJECT_VERSION = 1; 365 | DEVELOPMENT_ASSET_PATHS = "\"TriggerKitDemo/Preview Content\""; 366 | DEVELOPMENT_TEAM = AY32NZUWTJ; 367 | ENABLE_PREVIEWS = YES; 368 | GENERATE_INFOPLIST_FILE = YES; 369 | INFOPLIST_FILE = TriggerKitDemo/Info.plist; 370 | INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Connect to bluetooth MIDI devices"; 371 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 372 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 373 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 374 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 375 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 376 | LD_RUNPATH_SEARCH_PATHS = ( 377 | "$(inherited)", 378 | "@executable_path/Frameworks", 379 | ); 380 | MARKETING_VERSION = 1.0; 381 | PRODUCT_BUNDLE_IDENTIFIER = com.lightbeamapps.TriggerKitDemo; 382 | PRODUCT_NAME = "$(TARGET_NAME)"; 383 | SWIFT_EMIT_LOC_STRINGS = YES; 384 | SWIFT_VERSION = 5.0; 385 | TARGETED_DEVICE_FAMILY = "1,2"; 386 | }; 387 | name = Release; 388 | }; 389 | /* End XCBuildConfiguration section */ 390 | 391 | /* Begin XCConfigurationList section */ 392 | 55ABA2852A0251E0004A84E5 /* Build configuration list for PBXProject "TriggerKitDemo" */ = { 393 | isa = XCConfigurationList; 394 | buildConfigurations = ( 395 | 55ABA2962A0251E1004A84E5 /* Debug */, 396 | 55ABA2972A0251E1004A84E5 /* Release */, 397 | ); 398 | defaultConfigurationIsVisible = 0; 399 | defaultConfigurationName = Release; 400 | }; 401 | 55ABA2982A0251E1004A84E5 /* Build configuration list for PBXNativeTarget "TriggerKitDemo" */ = { 402 | isa = XCConfigurationList; 403 | buildConfigurations = ( 404 | 55ABA2992A0251E1004A84E5 /* Debug */, 405 | 55ABA29A2A0251E1004A84E5 /* Release */, 406 | ); 407 | defaultConfigurationIsVisible = 0; 408 | defaultConfigurationName = Release; 409 | }; 410 | /* End XCConfigurationList section */ 411 | 412 | /* Begin XCSwiftPackageProductDependency section */ 413 | 558463BC2A02750800646730 /* TriggerKit */ = { 414 | isa = XCSwiftPackageProductDependency; 415 | productName = TriggerKit; 416 | }; 417 | /* End XCSwiftPackageProductDependency section */ 418 | }; 419 | rootObject = 55ABA2822A0251E0004A84E5 /* Project object */; 420 | } 421 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "midikit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/orchetect/MIDIKit", 7 | "state" : { 8 | "revision" : "e092e53f3e5aafb628c0842931907173967e86de", 9 | "version" : "0.8.8" 10 | } 11 | }, 12 | { 13 | "identity" : "timecodekit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/orchetect/TimecodeKit", 16 | "state" : { 17 | "revision" : "6f65472a671fef760ca8fb2e9ca7cc25bc220aad", 18 | "version" : "1.6.10" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "notification40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "notification60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "settings58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "settings87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "spotlight80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "spotlight120.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "iphone120.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "iphone180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "ipadNotification20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "ipadNotification40.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "ipadSettings29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "ipadSettings58.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "ipadSpotlight40.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "ipadSpotlight80.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "ipad76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "ipad152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "ipadPro167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "appstore1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | }, 111 | { 112 | "filename" : "mac16.png", 113 | "idiom" : "mac", 114 | "scale" : "1x", 115 | "size" : "16x16" 116 | }, 117 | { 118 | "filename" : "mac32.png", 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "16x16" 122 | }, 123 | { 124 | "filename" : "mac32.png", 125 | "idiom" : "mac", 126 | "scale" : "1x", 127 | "size" : "32x32" 128 | }, 129 | { 130 | "filename" : "mac64.png", 131 | "idiom" : "mac", 132 | "scale" : "2x", 133 | "size" : "32x32" 134 | }, 135 | { 136 | "filename" : "mac128.png", 137 | "idiom" : "mac", 138 | "scale" : "1x", 139 | "size" : "128x128" 140 | }, 141 | { 142 | "filename" : "mac256.png", 143 | "idiom" : "mac", 144 | "scale" : "2x", 145 | "size" : "128x128" 146 | }, 147 | { 148 | "filename" : "mac256.png", 149 | "idiom" : "mac", 150 | "scale" : "1x", 151 | "size" : "256x256" 152 | }, 153 | { 154 | "filename" : "mac512.png", 155 | "idiom" : "mac", 156 | "scale" : "2x", 157 | "size" : "256x256" 158 | }, 159 | { 160 | "filename" : "mac512.png", 161 | "idiom" : "mac", 162 | "scale" : "1x", 163 | "size" : "512x512" 164 | }, 165 | { 166 | "filename" : "mac1024.png", 167 | "idiom" : "mac", 168 | "scale" : "2x", 169 | "size" : "512x512" 170 | } 171 | ], 172 | "info" : { 173 | "author" : "xcode", 174 | "version" : 1 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/appstore1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/appstore1024.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipad152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipad152.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipad76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipad76.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadPro167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadPro167.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/iphone120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/iphone120.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/iphone180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/iphone180.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac1024.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac128.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac16.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac256.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac32.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac512.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/mac64.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/notification40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/notification40.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/notification60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/notification60.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/settings58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/settings58.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/settings87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/settings87.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/spotlight120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/spotlight120.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/spotlight80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbeamapps/TriggerKit/4fc6f7bbd673b5ff48aacd13c254d0ab3c16ead3/Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/AppIcon.appiconset/spotlight80.png -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/logo.bluetooth.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "logo.bluetooth.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Assets.xcassets/logo.bluetooth.symbolset/logo.bluetooth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | Weight/Scale Variations 16 | Ultralight 17 | Thin 18 | Light 19 | Regular 20 | Medium 21 | Semibold 22 | Bold 23 | Heavy 24 | Black 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Design Variations 36 | Symbols are supported in up to nine weights and three scales. 37 | For optimal layout with text and other symbols, vertically align 38 | symbols with the adjacent text. 39 | 40 | 41 | 42 | 43 | 44 | Margins 45 | Leading and trailing margins on the left and right side of each symbol 46 | can be adjusted by modifying the x-location of the margin guidelines. 47 | Modifications are automatically applied proportionally to all 48 | scales and weights. 49 | 50 | 51 | 52 | Exporting 53 | Symbols should be outlined when exporting to ensure the 54 | design is preserved when submitting to Xcode. 55 | Template v.3.0 56 | Requires Xcode 13 or greater 57 | Generated from logo.bluetooth 58 | Typeset at 100 points 59 | Small 60 | Medium 61 | Large 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIBackgroundModes 6 | 7 | audio 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Model/AppActionsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppActions.swift 3 | // TriggerKitDemo 4 | // 5 | // Created by dave on 3/05/23. 6 | // 7 | 8 | import Foundation 9 | import TriggerKit 10 | 11 | public enum AppAction: String, Codable, Hashable, CaseIterable, Equatable { 12 | case updateSlider1 13 | case updateSlider2 14 | case updateSlider3 15 | case updateToggle1 16 | case updateToggle2 17 | } 18 | 19 | public class AppActionsViewModel: ObservableObject { 20 | 21 | // The mapping of the app action to the trigger, this is stored on disk 22 | public struct MidiNoteMapping: Codable, Hashable { 23 | var action: AppAction 24 | var trigger: TKTriggerMidiNote 25 | } 26 | 27 | // The mapping of the app action to the trigger, this is stored on disk 28 | public struct MidiCCMapping: Codable, Hashable { 29 | var action: AppAction 30 | var trigger: TKTriggerMidiCC 31 | } 32 | 33 | // MARK: - Published properties 34 | @Published var slider1: Double = 0.0 35 | @Published var slider2: Double = 0.0 36 | @Published var slider3: Double = 0.0 37 | @Published var toggle1: Bool = false 38 | @Published var toggle2: Bool = false 39 | @Published var currentEvent: TKEvent? 40 | @Published var currentMapping: TKMapping? 41 | 42 | @Published var midiLearn: Bool = false 43 | 44 | // MARK: - Private properties 45 | public let bus = TKBus(config: TKBusConfig(clientName: "TriggerKitDemo", 46 | model: "SwiftUI", 47 | manufacturer: "lightbeamapps", 48 | inputConnectionName: "TriggerKitDemo", 49 | decimalPlaces: 4)) 50 | 51 | init() { 52 | try? bus.midiStart() 53 | self.startup() 54 | } 55 | 56 | private func startup() { 57 | bus.setEventCallback { event in 58 | if let event { 59 | DispatchQueue.main.async { [unowned self] in 60 | self.currentEvent = event 61 | 62 | if var currentMapping = self.currentMapping { 63 | currentMapping.event = event 64 | 65 | let callback = callbackForAction(currentMapping.appAction) 66 | self.bus.addMapping(currentMapping, callback: callback) 67 | 68 | print("updating mapping") 69 | } 70 | } 71 | } 72 | } 73 | 74 | // Decode our mapped actions then loop through and all them appropriately 75 | // Register mappings 76 | bus.addMapping(.init(appAction: .updateSlider1, 77 | event: .midiCC(trigger: .init(cc: 2)))) { [unowned self] payload in 78 | self.updateSlider(slider: &slider1, value: payload.value) 79 | } 80 | 81 | bus.addMapping(.init(appAction: .updateSlider2, 82 | event: .midiCC(trigger: .init(cc: 3)))) { [unowned self] payload in 83 | self.updateSlider(slider: &slider2, value: payload.value) 84 | } 85 | 86 | bus.addMapping(.init(appAction: .updateSlider3, 87 | event: .midiCC(trigger: .init(cc: 4)))) { [unowned self] payload in 88 | self.updateSlider(slider: &slider3, value: payload.value) 89 | } 90 | 91 | bus.addMapping(.init(appAction: .updateToggle1, 92 | event: .midiCC(trigger: .init(cc: 23)))) { [unowned self] payload in 93 | self.flipToggle(toggle: &toggle1) 94 | } 95 | 96 | bus.addMapping(.init(appAction: .updateToggle2, 97 | event: .midiCC(trigger: .init(cc: 33)))) { [unowned self] payload in 98 | self.flipToggle(toggle: &toggle2) 99 | } 100 | } 101 | 102 | private func updateSlider(slider: inout Double, value: Double?) { 103 | guard let value else { return } 104 | slider = value 105 | } 106 | 107 | private func flipToggle(toggle: inout Bool) { 108 | toggle.toggle() 109 | } 110 | 111 | } 112 | 113 | extension AppActionsViewModel { 114 | public func selectMapping(_ mapping: TKMapping?) { 115 | self.currentMapping = mapping 116 | } 117 | } 118 | 119 | extension AppActionsViewModel { 120 | private func callbackForAction(_ action: AppAction) -> TKPayloadCallback { 121 | switch action { 122 | case .updateSlider1: 123 | return { [unowned self] payload in 124 | self.updateSlider(slider: &slider1, value: payload.value) 125 | } 126 | case .updateSlider2: 127 | return { [unowned self] payload in 128 | self.updateSlider(slider: &slider2, value: payload.value) 129 | } 130 | case .updateSlider3: 131 | return { [unowned self] payload in 132 | self.updateSlider(slider: &slider3, value: payload.value) 133 | } 134 | case .updateToggle1: 135 | return { [unowned self] payload in 136 | self.flipToggle(toggle: &toggle1) 137 | } 138 | case .updateToggle2: 139 | return { [unowned self] payload in 140 | self.flipToggle(toggle: &toggle2) 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Modifiers/PressActions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PressActions.swift 3 | // TriggerKitDemo 4 | // 5 | // Created by dave on 9/05/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PressActions: ViewModifier { 11 | var onPress: () -> Void 12 | var onRelease: () -> Void 13 | 14 | func body(content: Content) -> some View { 15 | content 16 | .simultaneousGesture( 17 | DragGesture(minimumDistance: 0) 18 | .onChanged({ _ in 19 | onPress() 20 | }) 21 | .onEnded({ _ in 22 | onRelease() 23 | }) 24 | ) 25 | } 26 | } 27 | 28 | 29 | extension View { 30 | func pressAction(onPress: @escaping (() -> Void), onRelease: @escaping (() -> Void)) -> some View { 31 | modifier(PressActions(onPress: { 32 | onPress() 33 | }, onRelease: { 34 | onRelease() 35 | })) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/TriggerKitDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TriggerKitDemoApp.swift 3 | // TriggerKitDemo 4 | // 5 | // Created by dave on 3/05/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct TriggerKitDemoApp: App { 12 | @StateObject var actionsViewModel: AppActionsViewModel = AppActionsViewModel() 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | AppTabView() 17 | .environmentObject(actionsViewModel) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Views/ActionsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionsView.swift 3 | // TriggerKitDemo 4 | // 5 | // Created by dave on 4/05/23. 6 | // 7 | 8 | import SwiftUI 9 | import TriggerKit 10 | 11 | struct ActionsView: View { 12 | @EnvironmentObject var actionsViewModel: AppActionsViewModel 13 | 14 | var body: some View { 15 | ZStack { 16 | List { 17 | ForEach(actionsViewModel.bus.mappings, id: \.self) { mapping in 18 | actionView(mapping: mapping) 19 | } 20 | } 21 | 22 | VStack { 23 | Spacer() 24 | self.midiReceiveView() 25 | } 26 | } 27 | .navigationTitle("Actions") 28 | .toolbar { 29 | ToolbarItem(placement: .navigationBarTrailing) { 30 | Toggle("Midi Learn", isOn: $actionsViewModel.midiLearn) 31 | } 32 | } 33 | } 34 | 35 | @ViewBuilder func actionView(mapping: TKMapping) -> some View { 36 | HStack { 37 | Text(mapping.appAction.rawValue) 38 | 39 | Spacer() 40 | 41 | Text(mapping.event.name()) 42 | } 43 | .background { 44 | Color(uiColor: .systemBackground) 45 | } 46 | .pressAction(onPress: { 47 | if self.actionsViewModel.midiLearn { 48 | print("select mapping") 49 | self.actionsViewModel.selectMapping(mapping) 50 | } 51 | }, onRelease: { 52 | print("deselect mapping") 53 | self.actionsViewModel.selectMapping(nil) 54 | }) 55 | } 56 | 57 | @ViewBuilder func midiReceiveView() -> some View { 58 | VStack { 59 | Text("Incoming midi events") 60 | HStack { 61 | Spacer() 62 | Text(actionsViewModel.currentEvent?.name() ?? "") 63 | .font(.caption) 64 | .padding(8) 65 | Spacer() 66 | } 67 | } 68 | .background { 69 | Color.blue 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Views/AppTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppTabView.swift 3 | // TriggerKitDemo 4 | // 5 | // Created by dave on 3/05/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppTabView: View { 11 | var body: some View { 12 | TabView { 13 | NavigationStack { 14 | DemoView() 15 | } 16 | .tabItem { 17 | Label("Demo", systemImage: "play.circle") 18 | } 19 | 20 | NavigationStack { 21 | ActionsView() 22 | } 23 | .tabItem { 24 | Label("Mapping", systemImage: "switch.2") 25 | } 26 | 27 | NavigationStack { 28 | TKBluetoothMIDIView() 29 | } 30 | .tabItem { 31 | Label("Connect", image: "logo.bluetooth") 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Examples/TriggerKitDemo/TriggerKitDemo/Views/DemoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoView.swift 3 | // TriggerKitDemo 4 | // 5 | // Created by dave on 4/05/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DemoView: View { 11 | @EnvironmentObject var actionsViewModel: AppActionsViewModel 12 | 13 | var body: some View { 14 | VStack { 15 | Slider(value: $actionsViewModel.slider1) { 16 | Text("1") 17 | } 18 | Slider(value: $actionsViewModel.slider2) { 19 | Text("2") 20 | } 21 | 22 | Slider(value: $actionsViewModel.slider3) { 23 | Text("3") 24 | } 25 | 26 | HStack { 27 | Toggle("1", isOn: $actionsViewModel.toggle1) 28 | Spacer() 29 | .padding() 30 | Toggle("2", isOn: $actionsViewModel.toggle2) 31 | } 32 | 33 | } 34 | .navigationTitle("Demo") 35 | .padding() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, David Gary Wood 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "midikit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/orchetect/MIDIKit", 7 | "state" : { 8 | "revision" : "630e42255190701f58b373c41862796e766ff018", 9 | "version" : "0.8.9" 10 | } 11 | }, 12 | { 13 | "identity" : "timecodekit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/orchetect/TimecodeKit", 16 | "state" : { 17 | "revision" : "6f65472a671fef760ca8fb2e9ca7cc25bc220aad", 18 | "version" : "1.6.10" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 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: "TriggerKit", 8 | platforms: [ 9 | .macOS(.v12), 10 | .iOS(.v14), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "TriggerKit", 16 | targets: ["TriggerKit"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | .package(url: "https://github.com/orchetect/MIDIKit", from: "0.8.8"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "TriggerKit", 28 | dependencies: [.product(name: "MIDIKit", package: "MIDIKit")]), 29 | .testTarget( 30 | name: "TriggerKitTests", 31 | dependencies: ["TriggerKit"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # TriggerKit 4 | 5 |

Bind MIDI events to code blocks in your application

6 | 7 |

8 | BSD 3-clause 9 |

10 | 11 | TriggerKit is a Swift framework for binding events to your app's code. You can use TriggerKit with iOS and iPad applications. MacOS is enabled in the Package, but not currently tested. 12 | 13 | TriggerKit is currently in Beta, and as such, may have breaking changes in future releases. When the planned event types are added, I will issue a 1.0 release. 14 | 15 | I would like to thank [Steffan Andrews](https://github.com/orchetect) for their [MIDIKit](https://github.com/orchetect/MIDIKit) library, without which TriggerKit would not be able to exist in it's current form. 16 | 17 | The key concept for TriggerKit, is that you can create codable actions for you app and events, and easily store and restore mappings for MIDI and other event types. 18 | 19 | ## Event Types supported 20 | - MIDI CC ✅ 21 | - MIDI Notes ✅ 22 | 23 | ## Event Types planned 🗺️ 24 | - OSC 25 | - Gamepad 26 | 27 | These event types are planned as additions to TriggerKit, in future releases. Please don't hesitate to open an issue or create a PR for features you need 🙏 28 | 29 | ## Quick start 🏁 30 | 31 | - Add TriggerKit to your project via Swift Package Manager: `https://github.com/lightbeamapps/TriggerKit` 32 | 33 | - Create an enumeration of the actions your app wants to map, that conforms to TKAppActionConstraints 34 | 35 | - Instantiate the TriggerKit Bus 36 | 37 | ```swift 38 | let config = TKBusConfig(clientName: "TriggerKit", model: "TriggerKit", manufacturer: "Lightbeam Apps") 39 | let bus = TKBus(config: config) 40 | ``` 41 | 42 | - Add Mappings 43 | 44 | ```swift 45 | let mapping = TKMapping(appAction: TestAcYourAppActiontion.action1, event: TKEvent.midiCC(trigger: .init(cc: 1))) 46 | 47 | bus.addMapping(mapping) { payload in 48 | // Do something! 49 | } 50 | 51 | ``` 52 | 53 | You now have a TriggerKit Bus, and your first mapping 🎉! 54 | 55 | ## Usage and key concepts 56 | 57 | ### Codable 58 | Once you have created actions and mappings, you can encode these to persist them to disk and retrieve at run time. 59 | 60 | ### Learning events 61 | You can set a single callback for all events with the bus function: 62 | 63 | ```swift 64 | bus.setEventCallback() { event in 65 | // Use/persist the most recent event to update or add a mapping 66 | } 67 | ``` 68 | 69 | The demo application shows an example of this. 70 | 71 | ## Examples 72 | - [TriggerKitDemo](https://github.com/lightbeamapps/TriggerKit/tree/main/Examples/TriggerKitDemo/) - a SwiftUI app that shows example mappings, and an event learn set up. 73 | 74 | ### Inbuilt views for Bluetooth midi 75 | 76 | TriggerKit has some wrappers for the CoreAudio BlueTooth MIDI detection views: 77 | - `TKBluetoothMIDIView` SwiftUI 78 | - `TKBTMIDICentralViewController` UIKit 79 | 80 | ## Contributing 81 | 82 | ### Code of Conduct and Contributing rules 🧑‍⚖️ 83 | 84 | - Our guide to contributing is available here: [CONTRIBUTING.md](CONTRIBUTING.md). 85 | - All contributions, pull requests, and issues are expected to adhere to our community code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). 86 | 87 | ### Key contributors ⚡️ 88 | 89 | #### Admin 90 | - [David Gary Wood](https://social.davidgarywood.com/@davidgarywood) 91 | 92 | ## License 📃 93 | 94 | TriggerKit is licensed with the BSD-3-Clause license, more information here: [LICENSE.MD](LICENSE.md) 95 | 96 | This is a permissive license which allows for any type of use, provided the copyright notice is included. 97 | 98 | ## Acknowledgements 🙏 99 | 100 | - MIDIKit, without which this library couldn't support MIDI Events: https://github.com/orchetect/MIDIKit 101 | -------------------------------------------------------------------------------- /Sources/TriggerKit/Model/TKAppActionConstraints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TKAppActionConstraints.swift 3 | // 4 | // 5 | // Created by dave on 4/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// App Actions supplied have to adhere to these constraints 11 | public typealias TKAppActionConstraints = Codable & CaseIterable & Hashable & Identifiable 12 | -------------------------------------------------------------------------------- /Sources/TriggerKit/Model/TKEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TriggerType.swift 3 | // 4 | // 5 | // Created by dave on 3/05/23. 6 | // 7 | 8 | import Foundation 9 | import MIDIKit 10 | 11 | /// The callback executed when an event is received by the TKBus object. Typically used for event learn features in the application 12 | public typealias TKEventCallback = (TKEvent?) -> (Void) 13 | 14 | /// Wrapper for different trigger types, to transport the most recent one back for MIDI learn features 15 | public enum TKEvent: Codable, Hashable { 16 | case midiNote(trigger: TKTriggerMidiNote) 17 | case midiCC(trigger: TKTriggerMidiCC) 18 | 19 | internal static func createEventFrom(midiEvent: MIDIEvent) -> TKEvent? { 20 | switch midiEvent { 21 | case .noteOn(let noteOn): 22 | let trigger = TKTriggerMidiNote(note: Int(noteOn.note.number), noteString: noteOn.note.stringValue()) 23 | return .midiNote(trigger: trigger) 24 | case .cc(let ccEvent): 25 | let trigger = TKTriggerMidiCC(cc: Int(ccEvent.controller.number)) 26 | return .midiCC(trigger: trigger) 27 | default: 28 | return nil 29 | } 30 | } 31 | 32 | public func name() -> String { 33 | switch self { 34 | case .midiCC(let trigger): 35 | return "CC: \(trigger.cc)" 36 | case .midiNote(let trigger): 37 | return "Note: \(trigger.noteString), \(trigger.note)" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/TriggerKit/Model/TKPayLoad.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TriggerPayLoad.swift 3 | // 4 | // 5 | // Created by dave on 3/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The callback triggered, with the payload corresponding to the event 11 | public typealias TKPayloadCallback = (TKPayLoad) -> Void 12 | 13 | public struct TKPayLoad: Codable, Hashable { 14 | /// Ranges from -1 to 1 for a gamepad axis, from 0-1.0 for CC values 15 | public var value: Double 16 | 17 | // This will carry the velocity for midi notes 18 | public var value2: Double? 19 | 20 | // Nil for CCs, the note for notes, and directly the message if available for OSC 21 | public var message: String? 22 | 23 | /// - Parameters: 24 | /// - value: the value of the event (ranging from 0.0-1.0) or nil 25 | /// - value2: the secondary value of the event (ranging from 0.0-1.0) or nil 26 | /// - message: Nil for CCs, the note for notes, and directly the message if available for OSC 27 | internal init(value: Double, value2: Double? = nil, message: String? = nil) { 28 | self.value = value 29 | self.value2 = value2 30 | self.message = message 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/TriggerKit/Model/TKTriggerMidiCC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MidiCCTrigger.swift 3 | // 4 | // 5 | // Created by dave on 3/05/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents a MIDI CC trigger with a CC value 11 | public struct TKTriggerMidiCC: Codable, Hashable { 12 | /// the CC value of the midi trigger 13 | public var cc: Int 14 | 15 | /// - Parameter cc: the CC value of the midi trigger 16 | public init(cc: Int) { 17 | self.cc = cc 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/TriggerKit/Model/TKTriggerMidiNote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MidiNoteTrigger.swift 3 | // 4 | // 5 | // Created by dave on 3/05/23. 6 | // 7 | 8 | import Foundation 9 | import MIDIKit 10 | 11 | /// Represents a MIDI note trigger 12 | public struct TKTriggerMidiNote: Codable, Hashable { 13 | /// The int value of the note being triggered, e.g 62 14 | public var note: Int 15 | 16 | /// The string value fo the note being triggered, e.g. "D3" 17 | public var noteString: String 18 | 19 | /// True if the note is being held down, false if being released 20 | public var noteOn: Bool 21 | 22 | /// MIDI Note initializer 23 | /// - Parameters: 24 | /// - note: The int value of the note being triggered, e.g 62 25 | /// - noteString: The string value fo the note being triggered, e.g. "D3" 26 | /// - noteOn: True if the note is being held down, false if being released 27 | public init(note: Int, noteString: String? = nil, noteOn: Bool = true) { 28 | self.note = note 29 | if let noteString { 30 | self.noteString = noteString 31 | } else if let midiNote = try? MIDINote.init(note) { 32 | self.noteString = midiNote.stringValue() 33 | } else { 34 | self.noteString = String(note) 35 | } 36 | 37 | self.noteOn = noteOn 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/TriggerKit/TKBus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TKBus.swift 3 | // 4 | // 5 | // Created by dave on 3/05/23. 6 | // 7 | 8 | import MIDIKitIO 9 | import Foundation 10 | import Combine 11 | 12 | /// Configuration for the TriggerKit Bus 13 | /// 14 | /// This is given to TKBus when it is initialized, containing all configurable values 15 | public struct TKBusConfig { 16 | internal var clientName: String 17 | internal var model: String 18 | internal var manufacturer: String 19 | internal var inputConnectionName: String = "TriggerKit" 20 | internal var decimalPlaces: Int 21 | 22 | /// Configuration for the TriggerKit Bus 23 | /// - Parameters: 24 | /// - clientName: Name identifying this instance, used via MIDIKit as a Core MIDI client ID 25 | /// - model: The name of your client application, used by MIDIKit 26 | /// - manufacturer: The name of your company used by MIDIKit 27 | /// created by the manager. 28 | /// - decimalPlaces: the number of decimal places MIDI CC values are rounded to 29 | public init(clientName: String, 30 | model: String, 31 | manufacturer: String, 32 | decimalPlaces: Int = 2, 33 | throttleRate: Double = 1 / 120) { 34 | self.clientName = clientName 35 | self.model = model 36 | self.manufacturer = manufacturer 37 | self.decimalPlaces = decimalPlaces 38 | } 39 | } 40 | 41 | /// Represents the a unique mapping of an app action to an event 42 | /// 43 | /// Mappings are registered with TKBus in order to associate events, and app actions, to blocks of code to be triggered. 44 | public struct TKMapping: Hashable, Codable, Identifiable where V: TKAppActionConstraints { 45 | /// The unique id of the mapping 46 | public var id: UUID 47 | /// Your application's enum representing an action that the app has 48 | public var appAction: V 49 | /// The TriggerKit event that triggers the appAction and associated block of code 50 | public var event: TKEvent? 51 | 52 | /// Represents the a unique mapping of an app action to an event 53 | /// - Parameters: 54 | /// - id: the unique ID of the mapping. This can be supplied by the client app if decoding existing mappings, or created by the initializer itself. 55 | /// - appAction: Your application's enum representing an action that the app has 56 | /// - event: The TriggerKit event that triggers the appAction and associated block of code 57 | public init(id: UUID = UUID(), appAction: V, event: TKEvent?) { 58 | self.id = id 59 | self.appAction = appAction 60 | self.event = event 61 | } 62 | } 63 | 64 | /// Core TriggerKit Bus object 65 | /// 66 | /// This is the core TriggerKit bus, that your application will use to hold mappings in memory and trigger them according to the events supplied. 67 | public class TKBus: ObservableObject where V: TKAppActionConstraints { 68 | // MARK: - Published public properties 69 | 70 | /// The latest event received 71 | @Published public var event: TKEvent? 72 | 73 | /// The current mappings associated with events 74 | @Published public var mappings: [TKMapping] = [] 75 | 76 | // MARK: - Published private properties 77 | 78 | /// The latest MIDI Note event 79 | @Published private var latestNoteEvent: MIDIEvent? 80 | 81 | /// The latest MIDI CC event 82 | @Published private var latestCCEvent: MIDIEvent? 83 | 84 | // MARK: - Public properties 85 | public var midiManager: MIDIManager 86 | 87 | // MARK: - Private properties 88 | 89 | /// The configuration supplied by the application 90 | private var config: TKBusConfig 91 | 92 | /// A look-up for the callbacks to call when events are triggered, linked by the UUID 93 | private var callbacks: [UUID: TKPayloadCallback] = [:] 94 | 95 | /// A single callback for each event, to enable event learning in the application 96 | private var eventCallback: TKEventCallback? 97 | 98 | /// The store for TriggerKit's Combine bindings 99 | private var cancellables = Set() 100 | 101 | /// A value for the input connection, used with MIDIKit 102 | private var inputConnection: MIDIInputConnection? { 103 | midiManager.managedInputConnections[config.inputConnectionName] 104 | } 105 | 106 | // MARK: - Initialization 107 | 108 | /// Initialize the TKBus object 109 | /// - Parameter config: A struct containing all configurable values 110 | public init(config: TKBusConfig) { 111 | self.config = config 112 | 113 | midiManager = MIDIManager( 114 | clientName: config.clientName, 115 | model: config.model, 116 | manufacturer: config.manufacturer 117 | ) 118 | 119 | self.setBindings() 120 | } 121 | 122 | /// Bindings triggered when events are received from MIDI 123 | private func setBindings() { 124 | self.$latestCCEvent 125 | .sink { event in 126 | guard let event else { return } 127 | self.handleMidiEvent(event) 128 | } 129 | .store(in: &cancellables) 130 | 131 | self.$latestNoteEvent 132 | .sink { event in 133 | guard let event else { return } 134 | self.handleMidiEvent(event) 135 | } 136 | .store(in: &cancellables) 137 | } 138 | 139 | /// Called when you are ready for the TKBus to set up connections and listen for events 140 | public func start() throws { 141 | try midiManager.start() 142 | 143 | try midiManager.addInputConnection( 144 | toOutputs: [], // no need to specify if we're using .allEndpoints 145 | tag: config.inputConnectionName, 146 | mode: .allEndpoints, // auto-connect to all outputs that may appear 147 | filter: .owned(), // don't allow self-created virtual endpoints 148 | receiver: .events({ [weak self] events in 149 | self?.processMidiEvents(events) 150 | }) 151 | ) 152 | } 153 | 154 | /// Private handler method for event arrays received from the MIDI manager 155 | private func processMidiEvents(_ events: [MIDIEvent]) { 156 | events.forEach({ event in 157 | handleMidiEvent(event) 158 | }) 159 | } 160 | 161 | /// Handles a single event 162 | /// - Parameter midiEvent: handles each single midi event, and ensure that callbacks are called on the main thread 163 | internal func handleMidiEvent(_ midiEvent: MIDIEvent) { 164 | guard 165 | let event = TKEvent.createEventFrom(midiEvent: midiEvent), 166 | let payload = createPayload(midiEvent) 167 | else { 168 | return 169 | } 170 | 171 | DispatchQueue.main.async { [weak self] in 172 | self?.eventCallback?(event) 173 | } 174 | 175 | let mappings = self.mappings.filter({ $0.event == event }) 176 | 177 | mappings.forEach { mapping in 178 | guard let callback = self.callbacks[mapping.id] else { return } 179 | 180 | DispatchQueue.main.async { 181 | callback(payload) 182 | } 183 | } 184 | } 185 | 186 | /// Creates a PayLoad from a midi event 187 | /// 188 | /// - Parameter event: the midi event to create a payload from 189 | /// - Returns: a standard TK payload struct 190 | internal func createPayload(_ event: MIDIEvent) -> TKPayLoad? { 191 | switch event { 192 | case .noteOn(let noteOn): 193 | return createPayload(value: Double(noteOn.note.number), value2: noteOn.velocity.unitIntervalValue) 194 | case .cc(let ccEvent): 195 | return createPayload(value: ccEvent.value.unitIntervalValue) 196 | default: 197 | break 198 | } 199 | 200 | return nil 201 | } 202 | 203 | } 204 | 205 | // MARK: - TKEvent Mappings 206 | extension TKBus { 207 | 208 | /// Add Mapping 209 | /// 210 | /// This updates an existing mapping if the UUID matches one held, OR adds a new one at the end of the mappings array if not. Executes changes to the mappings property on the main thread. 211 | /// - Parameters: 212 | /// - newMapping: the mapping to be created 213 | /// - callback: the callback to call when the mapping's event is received. 214 | public func addMapping(_ newMapping: TKMapping, callback: TKPayloadCallback?) { 215 | DispatchQueue.main.async { [weak self] in 216 | if let index = self?.mappings.firstIndex(where: { mapping in 217 | mapping.id == newMapping.id 218 | }) { 219 | self?.mappings[index] = newMapping 220 | } else { 221 | self?.mappings.append(newMapping) 222 | } 223 | 224 | self?.callbacks[newMapping.id] = nil 225 | 226 | self?.callbacks[newMapping.id] = callback 227 | } 228 | } 229 | 230 | public func removeMapping(_ mapping: TKMapping) { 231 | DispatchQueue.main.async { [weak self] in 232 | self?.mappings.removeAll { item in 233 | item.id == mapping.id 234 | } 235 | 236 | self?.callbacks[mapping.id] = nil 237 | } 238 | } 239 | 240 | } 241 | 242 | // MARK: - Event Mappings 243 | extension TKBus { 244 | /// Updates the event callback that is executed when a new event is received. This supports event learning in the application. 245 | /// - Parameter callback: The callback to be called 246 | public func setEventCallback(_ callback: TKEventCallback?) { 247 | eventCallback = callback 248 | } 249 | 250 | /// Removes the current event bacllback that is executed when a new event is received. 251 | public func removeEventCallback() { 252 | eventCallback = nil 253 | } 254 | } 255 | 256 | // MARK: - Convenience functions 257 | extension TKBus { 258 | 259 | /// Creates a payload based on a the values provided. Used internally to have one spot where this happens 260 | /// 261 | /// - Parameters: 262 | /// - value: the main value of the payload 263 | /// - value2: the secondary value of the paylaod 264 | /// - message: some events pass messages back. This is future proofing for support for things like OSC where events can have some extra meta data supplied. 265 | /// - Returns: a TK Payload struct 266 | internal func createPayload(value: Double, value2: Double? = nil, message: String? = nil) -> TKPayLoad { 267 | let payload = TKPayLoad(value: roundDouble(value) ?? 0, 268 | value2: roundDouble(value2), 269 | message: message) 270 | return payload 271 | } 272 | 273 | internal func roundDouble(_ value: Double?) -> Double? { 274 | guard let value else { return nil } 275 | 276 | let multiplier = pow(Double(10), Double(config.decimalPlaces)) 277 | 278 | let roundedValue = Double(round(multiplier * value)) 279 | return roundedValue / multiplier 280 | } 281 | 282 | internal func logEvent(_ event: TKEvent?) { 283 | DispatchQueue.main.async { [weak self] in 284 | self?.eventCallback?(event) 285 | } 286 | } 287 | 288 | } 289 | -------------------------------------------------------------------------------- /Sources/TriggerKit/Views/TKBluetoothMIDIView.swift: -------------------------------------------------------------------------------- 1 | // Shamelessly 'acquired' from MIDIKit, with grateful thanks to Steffan Andrews' work. 2 | // BluetoothMIDIView.swift 3 | // MIDIKit • https://github.com/orchetect/MIDIKit 4 | // © 2021-2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(iOS) 8 | 9 | import UIKit 10 | import SwiftUI 11 | import CoreAudioKit 12 | 13 | /// A SwiftUI view that wraps the CABTMIDICentralViewController 14 | public struct TKBluetoothMIDIView: UIViewControllerRepresentable { 15 | public init() { 16 | } 17 | 18 | public func makeUIViewController(context: Context) -> TKBTMIDICentralViewController { 19 | TKBTMIDICentralViewController() 20 | } 21 | 22 | public func updateUIViewController( 23 | _ uiViewController: TKBTMIDICentralViewController, 24 | context: Context 25 | ) { } 26 | 27 | public typealias UIViewControllerType = TKBTMIDICentralViewController 28 | } 29 | 30 | /// A UIKit view controller that wraps the CABTMIDICentralViewController 31 | public class TKBTMIDICentralViewController: CABTMIDICentralViewController { 32 | public var uiViewController: UIViewController? 33 | 34 | override public func viewDidLayoutSubviews() { 35 | super.viewDidLayoutSubviews() 36 | 37 | navigationItem.rightBarButtonItem = UIBarButtonItem( 38 | barButtonSystemItem: .done, 39 | target: self, 40 | action: #selector(doneAction) 41 | ) 42 | } 43 | 44 | @objc 45 | public func doneAction() { 46 | uiViewController?.dismiss(animated: true, completion: nil) 47 | } 48 | } 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /Tests/TriggerKitTests/ConfigTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigTests.swift 3 | // 4 | // 5 | // Created by dave on 14/05/23. 6 | // 7 | 8 | import XCTest 9 | @testable import TriggerKit 10 | 11 | final class ConfigTests: XCTestCase { 12 | 13 | enum TestAction: TKAppActionConstraints { 14 | case testAction1 15 | case testAction2 16 | case testAction3 17 | } 18 | 19 | func testStandardRoundingUp() { 20 | // Given 21 | let config = TKBusConfig(clientName: "TriggerKit", model: "TriggerKit", manufacturer: "Lightbeam Apps") 22 | let bus = TKBus(config: config) 23 | let mapping = TKMapping(appAction: TestAction.testAction1, event: TKEvent.midiCC(trigger: .init(cc: 1))) 24 | 25 | let expectation = expectation(description: "testNonStandardRoundingUp") 26 | 27 | bus.addMapping(mapping) { payload in 28 | if payload.value == 0.12 { 29 | expectation.fulfill() 30 | } 31 | } 32 | 33 | waitForMainThread() 34 | 35 | // When 36 | bus.handleMidiEvent(.cc(1, value:.unitInterval(0.115), channel: 0)) 37 | 38 | // Then 39 | defaultWaitForExpectations() 40 | } 41 | 42 | func testNonStandardRoundingUp() { 43 | // Given 44 | let config = TKBusConfig(clientName: "TriggerKit", model: "TriggerKit", manufacturer: "Lightbeam Apps", decimalPlaces: 3) 45 | let bus = TKBus(config: config) 46 | let mapping = TKMapping(appAction: TestAction.testAction1, event: TKEvent.midiCC(trigger: .init(cc: 1))) 47 | 48 | let expectation = expectation(description: "testNonStandardRoundingUp") 49 | 50 | bus.addMapping(mapping) { payload in 51 | if payload.value == 0.112 { 52 | expectation.fulfill() 53 | } 54 | } 55 | 56 | waitForMainThread() 57 | 58 | // When 59 | bus.handleMidiEvent(.cc(1, value:.unitInterval(0.1115), channel: 0)) 60 | 61 | // Then 62 | defaultWaitForExpectations() 63 | } 64 | 65 | func testStandardRoundingDown() { 66 | // Given 67 | let config = TKBusConfig(clientName: "TriggerKit", model: "TriggerKit", manufacturer: "Lightbeam Apps") 68 | let bus = TKBus(config: config) 69 | let mapping = TKMapping(appAction: TestAction.testAction1, event: TKEvent.midiCC(trigger: .init(cc: 1))) 70 | 71 | let expectation = expectation(description: "testNonStandardRoundingUp") 72 | 73 | bus.addMapping(mapping) { payload in 74 | if payload.value == 0.11 { 75 | expectation.fulfill() 76 | } 77 | } 78 | 79 | waitForMainThread() 80 | 81 | // When 82 | bus.handleMidiEvent(.cc(1, value:.unitInterval(0.114), channel: 0)) 83 | 84 | // Then 85 | defaultWaitForExpectations() 86 | } 87 | 88 | func testNonStandardRoundingDown() { 89 | // Given 90 | let config = TKBusConfig(clientName: "TriggerKit", model: "TriggerKit", manufacturer: "Lightbeam Apps", decimalPlaces: 3) 91 | let bus = TKBus(config: config) 92 | let mapping = TKMapping(appAction: TestAction.testAction1, event: TKEvent.midiCC(trigger: .init(cc: 1))) 93 | 94 | let expectation = expectation(description: "testNonStandardRoundingUp") 95 | 96 | bus.addMapping(mapping) { payload in 97 | if payload.value == 0.111 { 98 | expectation.fulfill() 99 | } 100 | } 101 | 102 | waitForMainThread() 103 | 104 | // When 105 | bus.handleMidiEvent(.cc(1, value:.unitInterval(0.1114), channel: 0)) 106 | 107 | // Then 108 | defaultWaitForExpectations() 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Tests/TriggerKitTests/MappingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TriggerKit 3 | 4 | final class MappingTests: XCTestCase { 5 | 6 | private func createConfig() -> TKBusConfig { 7 | TKBusConfig(clientName: "TriggerKit", model: "TriggerKit", manufacturer: "Lightbeam Apps") 8 | } 9 | 10 | enum TestAction: TKAppActionConstraints { 11 | case testAction1 12 | case testAction2 13 | case testAction3 14 | } 15 | 16 | var bus: TKBus! 17 | 18 | override func setUp() async throws { 19 | try await super.setUp() 20 | 21 | bus = TKBus(config: self.createConfig()) 22 | } 23 | 24 | func testAddingAMapping() { 25 | // Given 26 | let expectation = self.expectation(description: "testAddingAMapping - called") 27 | let event = TKEvent.midiCC(trigger: .init(cc: 1)) 28 | let mapping = TKMapping(appAction: TestAction.testAction1, event: event) 29 | 30 | bus.addMapping(mapping) { payload in 31 | if payload.value == 0.5 { 32 | expectation.fulfill() 33 | } 34 | } 35 | 36 | // We have to wait for the main thread for our mapping to be active 37 | waitForMainThread() 38 | 39 | // When 40 | bus.handleMidiEvent(.cc(1, value: .unitInterval(0.5), channel: 0)) 41 | 42 | // Then 43 | self.defaultWaitForExpectations() 44 | } 45 | 46 | func testRemovingAMapping() { 47 | // Given 48 | let event = TKEvent.midiCC(trigger: .init(cc: 1)) 49 | let mapping = TKMapping(appAction: TestAction.testAction1, event: event) 50 | 51 | bus.addMapping(mapping) { _ in 52 | // do nothing 53 | } 54 | 55 | // We have to wait for the main thread for our mapping to be active 56 | waitForMainThread() 57 | 58 | XCTAssertEqual(bus.mappings.first, mapping) 59 | XCTAssertEqual(bus.mappings.count, 1) 60 | 61 | // When 62 | bus.removeMapping(mapping) 63 | 64 | // We have to wait for the main thread for our mapping to be removed 65 | waitForMainThread() 66 | 67 | // Then 68 | XCTAssertNil(bus.mappings.first) 69 | XCTAssertTrue(bus.mappings.isEmpty) 70 | } 71 | 72 | func testUpdatingAMapping() { 73 | // Given 74 | let event1 = TKEvent.midiCC(trigger: .init(cc: 1)) 75 | let mapping1 = TKMapping(appAction: TestAction.testAction1, event: event1) 76 | let event2 = TKEvent.midiCC(trigger: .init(cc: 2)) 77 | let mapping2 = TKMapping(appAction: TestAction.testAction2, event: event2) 78 | 79 | bus.addMapping(mapping1, callback: { _ in }) 80 | bus.addMapping(mapping2, callback: { _ in }) 81 | 82 | // We have to wait for the main thread for our mapping to be active 83 | waitForMainThread() 84 | 85 | XCTAssertEqual(bus.mappings[0], mapping1) 86 | XCTAssertEqual(bus.mappings[1], mapping2) 87 | XCTAssertEqual(bus.mappings.count, 2) 88 | XCTAssertEqual(bus.mappings[0].event, event1) 89 | XCTAssertEqual(bus.mappings[1].event, event2) 90 | 91 | // When 92 | let updatedEvent1 = TKEvent.midiNote(trigger: .init(note: 1, noteString: "1")) 93 | var mapping1_Updated = mapping1 94 | mapping1_Updated.event = updatedEvent1 95 | 96 | bus.addMapping(mapping1_Updated, callback: { _ in }) 97 | 98 | // We have to wait for the main thread for our mapping to be removed 99 | waitForMainThread() 100 | 101 | // Then 102 | XCTAssertEqual(bus.mappings[0], mapping1_Updated) 103 | XCTAssertEqual(bus.mappings[1], mapping2) 104 | XCTAssertEqual(bus.mappings.count, 2) 105 | XCTAssertEqual(bus.mappings[0].event, updatedEvent1) 106 | XCTAssertEqual(bus.mappings[1].event, event2) 107 | } 108 | 109 | func testAddingANewMapping() { 110 | // Given 111 | let event1 = TKEvent.midiCC(trigger: .init(cc: 1)) 112 | let mapping1 = TKMapping(appAction: TestAction.testAction1, event: event1) 113 | let event2 = TKEvent.midiCC(trigger: .init(cc: 2)) 114 | let mapping2 = TKMapping(appAction: TestAction.testAction2, event: event2) 115 | 116 | bus.addMapping(mapping1, callback: { _ in }) 117 | bus.addMapping(mapping2, callback: { _ in }) 118 | 119 | // We have to wait for the main thread for our mapping to be active 120 | waitForMainThread() 121 | 122 | XCTAssertEqual(bus.mappings[0], mapping1) 123 | XCTAssertEqual(bus.mappings[1], mapping2) 124 | XCTAssertEqual(bus.mappings.count, 2) 125 | XCTAssertEqual(bus.mappings[0].event, event1) 126 | XCTAssertEqual(bus.mappings[1].event, event2) 127 | 128 | let newEvent = TKEvent.midiNote(trigger: .init(note: 1, noteString: "1")) 129 | let newMapping = TKMapping(appAction: TestAction.testAction3, event: newEvent) 130 | 131 | bus.addMapping(newMapping, callback: { _ in }) 132 | 133 | // We have to wait for the main thread for our mapping to be removed 134 | waitForMainThread() 135 | 136 | // Then 137 | XCTAssertEqual(bus.mappings[0], mapping1) 138 | XCTAssertEqual(bus.mappings[1], mapping2) 139 | XCTAssertEqual(bus.mappings[2], newMapping) 140 | XCTAssertEqual(bus.mappings.count, 3) 141 | XCTAssertEqual(bus.mappings[0].event, event1) 142 | XCTAssertEqual(bus.mappings[1].event, event2) 143 | XCTAssertEqual(bus.mappings[2].event, newEvent) 144 | } 145 | 146 | func testDuplicatingAMapping() { 147 | // Given 148 | let event1 = TKEvent.midiCC(trigger: .init(cc: 1)) 149 | let mapping1 = TKMapping(appAction: TestAction.testAction1, event: event1) 150 | let mappingDupe = TKMapping(appAction: TestAction.testAction1, event: event1) 151 | 152 | XCTAssertTrue(bus.mappings.isEmpty) 153 | 154 | // When 155 | bus.addMapping(mapping1, callback: { _ in }) 156 | bus.addMapping(mappingDupe, callback: { _ in }) 157 | 158 | // We have to wait for the main thread for our mappings to be active 159 | waitForMainThread() 160 | 161 | // Then we should have both in the bus's mapping 162 | XCTAssertEqual(bus.mappings[0], mapping1) 163 | XCTAssertEqual(bus.mappings[1], mappingDupe) 164 | XCTAssertEqual(bus.mappings.count, 2) 165 | XCTAssertEqual(bus.mappings[0].event, event1) 166 | XCTAssertEqual(bus.mappings[1].event, event1) 167 | } 168 | 169 | func testDuplicatingAMappingTriggersBothCallbacks() { 170 | // Given 171 | let event1 = TKEvent.midiCC(trigger: .init(cc: 1)) 172 | let mapping1 = TKMapping(appAction: TestAction.testAction1, event: event1) 173 | let mappingDupe = TKMapping(appAction: TestAction.testAction1, event: event1) 174 | 175 | XCTAssertTrue(bus.mappings.isEmpty) 176 | 177 | let expectation1 = expectation(description: "expectation1") 178 | let expectation2 = expectation(description: "expectation2") 179 | 180 | bus.addMapping(mapping1, callback: { _ in 181 | expectation1.fulfill() 182 | }) 183 | 184 | bus.addMapping(mappingDupe, callback: { _ in 185 | expectation2.fulfill() 186 | }) 187 | 188 | // We have to wait for the main thread for our mappings to be active 189 | waitForMainThread() 190 | 191 | // When 192 | bus.handleMidiEvent(.cc(1, value: .unitInterval(0.5), channel: 0)) 193 | 194 | waitForMainThread() 195 | 196 | // Then 197 | defaultWaitForExpectations() 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Tests/TriggerKitTests/TestConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestConfig.swift 3 | // 4 | // 5 | // Created by dave on 14/05/23. 6 | // 7 | 8 | import XCTest 9 | 10 | enum TestConfig { 11 | static var defaultMaxWaitTime: TimeInterval = 1.0 12 | } 13 | 14 | extension XCTestCase { 15 | func defaultWaitForExpectations() { 16 | self.waitForExpectations(timeout: TestConfig.defaultMaxWaitTime) 17 | } 18 | } 19 | 20 | extension XCTestCase { 21 | func waitForMainThread() { 22 | let expectation = self.expectation(description: "TriggerKit - waiting for main thread") 23 | DispatchQueue.main.async { 24 | expectation.fulfill() 25 | } 26 | 27 | self.wait(for: [expectation], timeout: TestConfig.defaultMaxWaitTime) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------