├── .gitignore ├── Wrapper ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── dark.png │ │ ├── light.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json └── Wrapper.swift ├── Wrapper.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── LICENSE ├── example.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | xcuserdata -------------------------------------------------------------------------------- /Wrapper/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Wrapper/Assets.xcassets/AppIcon.appiconset/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inkandswitch/wrapper/HEAD/Wrapper/Assets.xcassets/AppIcon.appiconset/dark.png -------------------------------------------------------------------------------- /Wrapper/Assets.xcassets/AppIcon.appiconset/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inkandswitch/wrapper/HEAD/Wrapper/Assets.xcassets/AppIcon.appiconset/light.png -------------------------------------------------------------------------------- /Wrapper.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Wrapper/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.500", 9 | "green" : "1.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Wrapper/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "light.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "dark.png", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "filename" : "dark.png", 29 | "idiom" : "universal", 30 | "platform" : "ios", 31 | "size" : "1024x1024" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /example.ts: -------------------------------------------------------------------------------- 1 | // 👋 Ahoy traveller — the entry point for this code is at the bottom of the file. 2 | // Start reading from there if you want to see what the code does, 3 | // or read from the top down if you want to see what the code is. 4 | 5 | // This file contains all the code needed to integrate your web app with the Wrapper iPad app, 6 | // by receiving batches of WrapperEvent objects and forwarding them to your system. 7 | // It also does an automatic fallback to PointerEvents so your web app can run outside the Wrapper, too. 8 | // When running outside the Wrapper, the mouse/stylus is treated as a pencil. 9 | // If you hold down the spacebar the mouse will be treated as a finger. 10 | 11 | // A "touch" is a series of events from a single finger or the pencil. 12 | // You use this ID to figure out which finger/pencil an event corresponds to. 13 | // Every time you begin a new touch, it is assigned a new ID. 14 | export type TouchId = number 15 | 16 | export type WrapperEventType = "pencil" | "finger" 17 | export type WrapperEventPhase = "hover" | "began" | "moved" | "ended" 18 | export type Position = { x: number, y: number } 19 | 20 | export type WrapperEvent = { 21 | id: TouchId 22 | type: WrapperEventType 23 | phase: WrapperEventPhase 24 | predicted: boolean 25 | position: Position 26 | hoverHeight: number 27 | pressure: number 28 | altitude: number 29 | azimuth: number 30 | rollAngle: number 31 | radius: number 32 | timestamp: number 33 | } 34 | 35 | // iOS will synthesize extra events that anticipate where the Pencil will be in the near future. 36 | // Using these predicted events can reduce perceptual latency by ~1 frame, which is fantastic. 37 | // But, when the "real" event(s) arrive in the next batch, it's up to you to clean up any inconsistencies 38 | // introduced by the predictions. That's often complex, and not worthwhile for a quick prototype. 39 | const usePredictedEvents = false 40 | 41 | // This state helps us turn PointerEvents into the right sequence of WrapperEvents 42 | let pointerDown = false 43 | let spaceBarDown = false 44 | 45 | // Convert a single PointerEvent into a single WrapperEvent 46 | function pointerEvent(e: PointerEvent, phase: WrapperEventPhase) { 47 | if (phase === "began") pointerDown = true 48 | if (phase === "ended") pointerDown = false 49 | 50 | if (phase === "moved" && !pointerDown) phase = "hover" 51 | 52 | const type = e.pointerType == "touch" || spaceBarDown ? "finger" : "pencil" 53 | 54 | // Adjust as needed. 55 | // (The typical range of pressure values for the Apple Pencil is something like 0.5 to 3.5) 56 | const pressure = e.pointerType == "mouse" ? 1 : e.pressure * 5 57 | 58 | handleWrapperEvents([{ 59 | id: e.pointerId, 60 | type, 61 | phase, 62 | predicted: false, 63 | position: { x: e.clientX, y: e.clientY }, 64 | hoverHeight: .5, 65 | pressure, 66 | altitude: 0, 67 | azimuth: 0, 68 | rollAngle: 0, 69 | radius: 0, 70 | timestamp: performance.now() 71 | }]) 72 | } 73 | 74 | const preventDefault = (e: TouchEvent) => e.preventDefault() 75 | 76 | function setupFallbackEventListeners() { 77 | // Block the "swipe to go back/forward" gesture in Safari 78 | window.addEventListener("touchstart", preventDefault, { passive: false }) 79 | 80 | window.onpointerdown = (e: PointerEvent) => pointerEvent(e, "began") 81 | window.onpointermove = (e: PointerEvent) => pointerEvent(e, "moved") 82 | window.onpointerup = (e: PointerEvent) => pointerEvent(e, "ended") 83 | 84 | window.onkeydown = (e: KeyboardEvent) => { if (e.key === " ") spaceBarDown = true } 85 | window.onkeyup = (e: KeyboardEvent) => { if (e.key === " ") spaceBarDown = false } 86 | } 87 | 88 | function clearFallbackEventListeners() { 89 | window.onpointerdown = null 90 | window.onpointermove = null 91 | window.onpointerup = null 92 | window.removeEventListener("touchstart", preventDefault) 93 | window.onkeydown = null 94 | window.onkeyup = null 95 | } 96 | 97 | // The first time we receive a call from the Wrapper, we can remove the fallback listeners, since they're redundant. 98 | function handleFirstWrapperEvents(events: WrapperEvent[]) { 99 | // From now on, the Wrapper should call the function named handleWrapperEvents 100 | ;(window as any).wrapperEvents = handleWrapperEvents 101 | clearFallbackEventListeners() 102 | handleWrapperEvents(events) 103 | } 104 | 105 | function handleWrapperEvents(events: WrapperEvent[]) { 106 | for (const event of events) { 107 | if (event.predicted && !usePredictedEvents) continue 108 | 109 | // NOW IT'S YOUR TURN! 110 | // Do something with the event here. 111 | console.log(event) 112 | } 113 | } 114 | 115 | // 👋 Ahoy traveller — here's where the action begins. 116 | 117 | // We add some fallback listeners by default (to support running outside the Wrapper) 118 | // and remove them as soon as we have proof that we are, in fact, inside the Wrapper. 119 | setupFallbackEventListeners() 120 | 121 | // The Wrapper calls `window.wrapperEvents(...)` 122 | ;(window as any).wrapperEvents = handleFirstWrapperEvents 123 | -------------------------------------------------------------------------------- /Wrapper/Wrapper.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WebKit 3 | 4 | // Put your mDNS, IP address, or web URL here. 5 | // (Note: You can use a local web server with a self-signed cert, and https as the protocol, to (eg) get more accuracy from performance.now()) 6 | let url = URL(string: "http://my-mDNS-name.local:5173")! 7 | 8 | @main 9 | struct WrapperApp: App { 10 | var body: some Scene { 11 | WindowGroup { 12 | AppView() 13 | } 14 | } 15 | } 16 | 17 | struct AppView: View { 18 | @State private var error: Error? 19 | @State private var loading = true 20 | 21 | var body: some View { 22 | VStack { 23 | if let error = error { 24 | // In the event of an error, show the error message and a handy quit button (so you don't have to force-quit) 25 | Text(error.localizedDescription) 26 | .foregroundColor(.pink) 27 | .font(.headline) 28 | Button("Quit") { exit(EXIT_FAILURE) } 29 | .buttonStyle(.bordered) 30 | .foregroundColor(.primary) 31 | } else { 32 | // Load the WebView, and show a spinner while it's loading 33 | ZStack { 34 | WrapperWebView(error: $error, loading: $loading) 35 | .opacity(loading ? 0 : 1) // The WebView is opaque white while loading, which sucks in dark mode 36 | if loading { 37 | VStack(spacing: 20) { 38 | Text("Attempting to load \(url)") 39 | .foregroundColor(.gray) 40 | .font(.headline) 41 | ProgressView() 42 | } 43 | } 44 | } 45 | } 46 | } 47 | .ignoresSafeArea() // Allow views to stretch right to the edges 48 | .statusBarHidden() // Hide the status bar at the top 49 | .persistentSystemOverlays(.hidden) // Hide the home indicator at the bottom 50 | .defersSystemGestures(on:.all) // Block the first swipe from the top (todo: doesn't seem to block the bottom) 51 | // We also have fullScreenRequired set in the Project settings, so we're opted-out from multitasking 52 | } 53 | } 54 | 55 | // This struct wraps WKWebView so that we can use it in SwiftUI. 56 | // Hopefully it won't be long before this can all be removed. 57 | struct WrapperWebView: UIViewRepresentable { 58 | let webView = WKWebView() 59 | @Binding var error: Error? 60 | @Binding var loading: Bool 61 | 62 | func makeUIView(context: Context) -> WKWebView { 63 | webView.isInspectable = true 64 | webView.navigationDelegate = context.coordinator 65 | webView.addGestureRecognizer(TouchesToJS(webView)) 66 | webView.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData)) 67 | return webView 68 | } 69 | 70 | // Required by UIViewRepresentable 71 | func updateUIView(_ uiView: WKWebView, context: Context) {} 72 | 73 | // To make use of various WKWebView delegates, we need a real class 74 | func makeCoordinator() -> WebViewCoordinator { WebViewCoordinator(self) } 75 | class WebViewCoordinator: NSObject, WKNavigationDelegate { 76 | let parent: WrapperWebView 77 | 78 | init(_ webView: WrapperWebView) { self.parent = webView } 79 | 80 | // Handle loading success / failure 81 | func webView(_ wv: WKWebView, didFinish nav: WKNavigation) { parent.loading = false; } 82 | func webView(_ wv: WKWebView, didFail nav: WKNavigation, withError error: Error) { parent.error = error } 83 | func webView(_ wv: WKWebView, didFailProvisionalNavigation nav: WKNavigation, withError error: Error) { parent.error = error } 84 | 85 | // This makes the webview ignore certificate errors, so you can use a self-signed cert for https, so that the browser context is trusted, which enables additional APIs 86 | func webView(_ wv: WKWebView, respondTo challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { 87 | (.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 88 | } 89 | } 90 | } 91 | 92 | // This class captures gesture events triggered on a given WKWebView, and re-triggeres them inside its JS context. 93 | // This allows JS to receive pencil and touch simultaneously. 94 | class TouchesToJS: UIGestureRecognizer { 95 | 96 | let webView: WKWebView 97 | let hoverRecognizer: UIHoverGestureRecognizer 98 | 99 | init(_ webView: WKWebView) { 100 | self.webView = webView 101 | self.hoverRecognizer = UIHoverGestureRecognizer(target: nil, action: nil) 102 | super.init(target:nil, action:nil) 103 | 104 | requiresExclusiveTouchType = false // Allow simultaneous pen and touch events 105 | 106 | webView.addGestureRecognizer(self) 107 | hoverRecognizer.addTarget(self, action: #selector(handleHover)) 108 | webView.addGestureRecognizer(hoverRecognizer) 109 | } 110 | 111 | typealias TouchJSON = [String: AnyHashable] 112 | 113 | func sendTouches(_ phase: String, _ touches: Set, _ event: UIEvent) { 114 | for touch in touches { 115 | let id = touch.hashValue 116 | let coalesced = event.coalescedTouches(for: touch) ?? [touch] 117 | let jsonArr = coalesced.map { t -> TouchJSON in 118 | let location = t.preciseLocation(in: webView) 119 | return [ 120 | "id": id, 121 | "type": t.type == .pencil ? "pencil" : "finger", 122 | "phase": phase, 123 | "position": ["x": location.x, "y": location.y], 124 | "hoverHeight": 0, 125 | "pressure": t.force, 126 | "altitude": t.altitudeAngle, 127 | "azimuth": t.azimuthAngle(in: webView), 128 | "rollAngle": t.rollAngle, 129 | "radius": t.majorRadius, 130 | "timestamp": t.timestamp, 131 | ] 132 | } 133 | sendToJS(jsonArr) 134 | } 135 | } 136 | 137 | @objc func handleHover(_ recognizer: UIHoverGestureRecognizer) { 138 | let location = recognizer.location(in: webView) 139 | sendToJS([[ 140 | "id": recognizer.hashValue, 141 | "type": "pencil", 142 | "phase": "hover", 143 | "position": ["x": location.x, "y": location.y], 144 | "hoverHeight": recognizer.zOffset, 145 | "pressure": 0, 146 | "altitude": recognizer.altitudeAngle, 147 | "azimuth": recognizer.azimuthAngle(in: webView), 148 | "rollAngle": recognizer.rollAngle, 149 | "radius": 0, 150 | "timestamp": 0, 151 | ]]) 152 | } 153 | 154 | func sendToJS(_ jsonArr: [TouchJSON]) { 155 | if let data = try? JSONSerialization.data(withJSONObject: jsonArr), 156 | let jsonString = String(data: data, encoding: .utf8) { 157 | webView.evaluateJavaScript("if ('wrapperEvents' in window) wrapperEvents(\(jsonString))") 158 | } 159 | } 160 | 161 | override func touchesBegan (_ touches: Set, with event: UIEvent) { sendTouches("began", touches, event) } 162 | override func touchesMoved (_ touches: Set, with event: UIEvent) { sendTouches("moved", touches, event) } 163 | override func touchesEnded (_ touches: Set, with event: UIEvent) { sendTouches("ended", touches, event) } 164 | override func touchesCancelled(_ touches: Set, with event: UIEvent) { sendTouches("ended", touches, event) } // "ended" because we don't differentiate between ended and cancelled in the web app 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | At Ink & Switch, one of the things we're researching is a new kind of [dynamic notebook](https://www.inkandswitch.com/inkbase) — something like a pad of paper with a pen, but programmable. As part of this research we build a lot of little prototypes, using the web platform as a substrate for rapid iteration. We run these prototypes on iPad because the form factor (tablet + stylus) is very close to what we have in mind for our dynamic notebook. 2 | 3 | On iPad, however, you can't get simultaneous input from fingers and Apple Pencil in the browser. It just doesn't work. But this is exactly what we need for our prototypes: fluid, gestural input from both hands and the pen. 4 | 5 | Here's our workaround. 6 | 7 | This repo contains the Xcode project for a very simple iPad app. It opens a URL of your choice in a fullscreen Webkit view, captures all incoming touch and pencil events on the native/Swift side, then forwards them to your JS code. Your JS code then uses these events instead of the PointerEvents dispatched by the browser. 8 | 9 | This project exists to support rapid experimental prototyping. It's deliberately "batteries not included". You should rip it apart, [kitbashing](https://en.wikipedia.org/wiki/Kitbashing) whatever you need to quickly test your ideas. 10 | 11 | ## Setup 12 | 13 | ### 1. Build the iPad app 14 | 15 | 1. Clone this repo to your Mac, and open `Wrapper.xcodeproj` in Xcode. 16 | 17 | 2. In [`Wrapper.swift`](/Wrapper/Wrapper.swift), at the top, set the URL of your web app. Typically, you'll use the local IP / mDNS of a live-reloading dev server on your Mac (eg: [vite](https://vite.dev)). 18 | 19 | 3. Set a [run destination](https://developer.apple.com/documentation/xcode/building-and-running-an-app) (ie: tell Xcode to run your app on your iPad). 20 | 21 | You *might* also need to do the following. 22 | 23 | 4. In Signing & Capabilities, set a [Team and Bundle Identifier](https://developer.apple.com/documentation/xcode/preparing-your-app-for-distribution). The latter should be globally unique. 24 | 25 | Finally, do a `Build & Run` and if everything goes well, the app should launch on your iPad, and you'll see whatever is being served at the URL you entered. 26 | 27 | ### 2. Receive the pencil & touch events in your web app 28 | 29 | The iPad Pro emits finger events at 120 Hz and pencil events at 240 Hz. The app will batch-up all the input events that occur within a short window of time (say, 1 frame at 120 Hz), then call the function `window.wrapperEvents(...)` with each batch of events. Here's how you'd receive them in your web app: 30 | 31 | ```javascript 32 | window.wrapperEvents = (events) => { 33 | for (const event of events) { 34 | // Do something with this event 35 | } 36 | } 37 | ``` 38 | 39 | For a more detailed example with a few quality-of-life features, see [`example.ts`](/example.ts) 40 | 41 | ### 3. Submit to TestFlight 42 | 43 | After you've built something, you might want to share it with other folks so they can test it. Here are the steps for setting up a private TestFlight. 44 | 45 | Prerequisites: 46 | * A paid Apple developer team account that you are willing to share with your testers. 47 | * You must have the Admin or App Manager role on this Apple developer team. 48 | * The email address for the Apple IDs for each of your testers, so you can add them to your team. 49 | 50 | Steps: 51 | 1. Put a build of your web app somewhere on the internet — Surge, Netlify, Vercel, S3, doesn't matter. 52 | 2. In Xcode, change [`Wrapper.swift`](/Wrapper/Wrapper.swift) to load this URL. 53 | 3. Choose `Product > Archive`. This will build your project, then open the Organizer window. 54 | 4. Click "Validate App". Xcode may prompt you to fill in some basic info for the App Store. 55 | 5. If Validation fails, go fix whatever issues it surfaces. 56 | * The most common issue is that your bundle identifier isn't unique. Change it to something unique. (Note that you actually have to click somewhere outside the bundle identifier field for it to save the new value.) Then go back to step 3. 57 | 6. Once Validation succeeds, click "Distribute App". The default method ("App Store Connect") is fine. 58 | 7. When the upload finishes, click the link to open your app in App Store Connect. At the top, click the big TestFlight link. 59 | 8. In a new tab, open [Users and Access](https://appstoreconnect.apple.com/access/users), and create a New User for each of your testers. Give them the Developer role. 60 | 9. Go back to your app in TestFlight. In the left sidebar look for "Internal Testing", then click the blue (+) icon to create a group of users to test the app. Give your group a name. 61 | 10. You'll be presented with a list of users on your team. Select your testers. 62 | 63 | That should be all it takes. As soon as people are added to the group, they'll get an invite email. 64 | 65 | In our experience, this process is cumbersome and error-prone. Just give it your best shot. If something doesn't work, wait a few days and try again. 66 | 67 | ## Usage Notes 68 | 69 | ### Tips 70 | * Once you have the app installed on your iPad, you don't need to run Xcode again. Just make sure your dev server is running (ie: run `vite` or whatever), then launch the app. If something goes wrong, force quit the app (swipe it upward on the app switcher) and try again. 71 | * The events array passed to `window.wrapperEvents(...)` will interleave pencil and touch events, and (with one exception) all events arrive in the order they occur. 72 | * The exception is that some events are extrapolated *predictions* of where the pencil might go in the near future. These events will have `event.predicted === true`. In the following batch of events, you will get the real position that the pencil ended up going. 73 | * The events come in *fast*. It's a good idea to merge events together, and only act on the most current data for each touch. 74 | * You can use the Safari developer tools on your Mac to remotely debug your web app as it runs in the Wrapper. This tends to work well for seeing logs, inspecting elements, etc. It doesn't work very well for inspecting JS perf via the timeline. It can also be a bit finicky to get connected — using a cable is not strictly necessary, but it often helps. 75 | 76 | ### Limitations 77 | * You can't use stuff like `alert()` / `prompt()`, the clipboard, the `download` attr, etc… unless you implement support for that in Swift. 78 | * The iPad Pro screen refreshes at 120 Hz. As of iOS 18, you can enable a feature flag to run `requestAnimationFrame` at 120 Hz in Safari. But this feature flag doesn't apply to the Webkit view we use, so it's capped at 60 Hz. We don't know of any way to run at 120 Hz in WKWebView — if you figure this out, PLEASE tell us! 79 | * Pencil input is imperfect. While rare, we've sometimes struggled with dropped or sticky inputs, possibly due to false positives/negatives from palm rejection. (If you spot a way to improve this, PLEASE tell us!) 80 | * In our informal measurements, we typically see 3–4 frames (50ms–70ms) of motion-to-photon latency when using the Wrapper. This is about on par with PointerEvents generated by the browser, maybe slightly worse. Your mileage may vary. 81 | 82 | ### "Help!!" 83 | 84 | We don't offer any warranty or support for this project. But, feel free to ping [Ivan](http://mastodon.social/@spiralganglion) on Mastodon if you have quick questions, or want to share something cool you've made. 85 | 86 | Issues and PRs are also welcome, but are likely to be ignored or closed if they add complexity. 87 | -------------------------------------------------------------------------------- /Wrapper.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | 8485626D2D2C57750063928C /* Wrapper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Wrapper.app; sourceTree = BUILT_PRODUCTS_DIR; }; 11 | /* End PBXFileReference section */ 12 | 13 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 14 | 8485626F2D2C57750063928C /* Wrapper */ = { 15 | isa = PBXFileSystemSynchronizedRootGroup; 16 | path = Wrapper; 17 | sourceTree = ""; 18 | }; 19 | /* End PBXFileSystemSynchronizedRootGroup section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | 8485626A2D2C57750063928C /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | 848562642D2C57750063928C = { 33 | isa = PBXGroup; 34 | children = ( 35 | 8485626F2D2C57750063928C /* Wrapper */, 36 | 8485626E2D2C57750063928C /* Products */, 37 | ); 38 | sourceTree = ""; 39 | }; 40 | 8485626E2D2C57750063928C /* Products */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 8485626D2D2C57750063928C /* Wrapper.app */, 44 | ); 45 | name = Products; 46 | sourceTree = ""; 47 | }; 48 | /* End PBXGroup section */ 49 | 50 | /* Begin PBXNativeTarget section */ 51 | 8485626C2D2C57750063928C /* Wrapper */ = { 52 | isa = PBXNativeTarget; 53 | buildConfigurationList = 8485627B2D2C57760063928C /* Build configuration list for PBXNativeTarget "Wrapper" */; 54 | buildPhases = ( 55 | 848562692D2C57750063928C /* Sources */, 56 | 8485626A2D2C57750063928C /* Frameworks */, 57 | 8485626B2D2C57750063928C /* Resources */, 58 | ); 59 | buildRules = ( 60 | ); 61 | dependencies = ( 62 | ); 63 | fileSystemSynchronizedGroups = ( 64 | 8485626F2D2C57750063928C /* Wrapper */, 65 | ); 66 | name = Wrapper; 67 | packageProductDependencies = ( 68 | ); 69 | productName = Wrapper; 70 | productReference = 8485626D2D2C57750063928C /* Wrapper.app */; 71 | productType = "com.apple.product-type.application"; 72 | }; 73 | /* End PBXNativeTarget section */ 74 | 75 | /* Begin PBXProject section */ 76 | 848562652D2C57750063928C /* Project object */ = { 77 | isa = PBXProject; 78 | attributes = { 79 | BuildIndependentTargetsInParallel = 1; 80 | LastSwiftUpdateCheck = 1620; 81 | LastUpgradeCheck = 1620; 82 | TargetAttributes = { 83 | 8485626C2D2C57750063928C = { 84 | CreatedOnToolsVersion = 16.2; 85 | }; 86 | }; 87 | }; 88 | buildConfigurationList = 848562682D2C57750063928C /* Build configuration list for PBXProject "Wrapper" */; 89 | developmentRegion = en; 90 | hasScannedForEncodings = 0; 91 | knownRegions = ( 92 | en, 93 | Base, 94 | ); 95 | mainGroup = 848562642D2C57750063928C; 96 | minimizedProjectReferenceProxies = 1; 97 | preferredProjectObjectVersion = 77; 98 | productRefGroup = 8485626E2D2C57750063928C /* Products */; 99 | projectDirPath = ""; 100 | projectRoot = ""; 101 | targets = ( 102 | 8485626C2D2C57750063928C /* Wrapper */, 103 | ); 104 | }; 105 | /* End PBXProject section */ 106 | 107 | /* Begin PBXResourcesBuildPhase section */ 108 | 8485626B2D2C57750063928C /* Resources */ = { 109 | isa = PBXResourcesBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | /* End PBXResourcesBuildPhase section */ 116 | 117 | /* Begin PBXSourcesBuildPhase section */ 118 | 848562692D2C57750063928C /* Sources */ = { 119 | isa = PBXSourcesBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXSourcesBuildPhase section */ 126 | 127 | /* Begin XCBuildConfiguration section */ 128 | 848562792D2C57760063928C /* Debug */ = { 129 | isa = XCBuildConfiguration; 130 | buildSettings = { 131 | ALWAYS_SEARCH_USER_PATHS = NO; 132 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 133 | CLANG_ANALYZER_NONNULL = YES; 134 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 135 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 136 | CLANG_ENABLE_MODULES = YES; 137 | CLANG_ENABLE_OBJC_ARC = YES; 138 | CLANG_ENABLE_OBJC_WEAK = YES; 139 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 140 | CLANG_WARN_BOOL_CONVERSION = YES; 141 | CLANG_WARN_COMMA = YES; 142 | CLANG_WARN_CONSTANT_CONVERSION = YES; 143 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 144 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 145 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 146 | CLANG_WARN_EMPTY_BODY = YES; 147 | CLANG_WARN_ENUM_CONVERSION = YES; 148 | CLANG_WARN_INFINITE_RECURSION = YES; 149 | CLANG_WARN_INT_CONVERSION = YES; 150 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 151 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 152 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 153 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 154 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 155 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 156 | CLANG_WARN_STRICT_PROTOTYPES = YES; 157 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 158 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 159 | CLANG_WARN_UNREACHABLE_CODE = YES; 160 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 | COPY_PHASE_STRIP = NO; 162 | DEBUG_INFORMATION_FORMAT = dwarf; 163 | ENABLE_STRICT_OBJC_MSGSEND = YES; 164 | ENABLE_TESTABILITY = YES; 165 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 166 | GCC_C_LANGUAGE_STANDARD = gnu17; 167 | GCC_DYNAMIC_NO_PIC = NO; 168 | GCC_NO_COMMON_BLOCKS = YES; 169 | GCC_OPTIMIZATION_LEVEL = 0; 170 | GCC_PREPROCESSOR_DEFINITIONS = ( 171 | "DEBUG=1", 172 | "$(inherited)", 173 | ); 174 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 175 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 176 | GCC_WARN_UNDECLARED_SELECTOR = YES; 177 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 178 | GCC_WARN_UNUSED_FUNCTION = YES; 179 | GCC_WARN_UNUSED_VARIABLE = YES; 180 | INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; 181 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 182 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 183 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 184 | MTL_FAST_MATH = YES; 185 | ONLY_ACTIVE_ARCH = YES; 186 | SDKROOT = iphoneos; 187 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 188 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 189 | }; 190 | name = Debug; 191 | }; 192 | 8485627A2D2C57760063928C /* Release */ = { 193 | isa = XCBuildConfiguration; 194 | buildSettings = { 195 | ALWAYS_SEARCH_USER_PATHS = NO; 196 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 197 | CLANG_ANALYZER_NONNULL = YES; 198 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 199 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 200 | CLANG_ENABLE_MODULES = YES; 201 | CLANG_ENABLE_OBJC_ARC = YES; 202 | CLANG_ENABLE_OBJC_WEAK = YES; 203 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 204 | CLANG_WARN_BOOL_CONVERSION = YES; 205 | CLANG_WARN_COMMA = YES; 206 | CLANG_WARN_CONSTANT_CONVERSION = YES; 207 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 208 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 209 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 210 | CLANG_WARN_EMPTY_BODY = YES; 211 | CLANG_WARN_ENUM_CONVERSION = YES; 212 | CLANG_WARN_INFINITE_RECURSION = YES; 213 | CLANG_WARN_INT_CONVERSION = YES; 214 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 215 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 216 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 218 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 219 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 220 | CLANG_WARN_STRICT_PROTOTYPES = YES; 221 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 223 | CLANG_WARN_UNREACHABLE_CODE = YES; 224 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 225 | COPY_PHASE_STRIP = NO; 226 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 227 | ENABLE_NS_ASSERTIONS = NO; 228 | ENABLE_STRICT_OBJC_MSGSEND = YES; 229 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 230 | GCC_C_LANGUAGE_STANDARD = gnu17; 231 | GCC_NO_COMMON_BLOCKS = YES; 232 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 233 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 234 | GCC_WARN_UNDECLARED_SELECTOR = YES; 235 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 236 | GCC_WARN_UNUSED_FUNCTION = YES; 237 | GCC_WARN_UNUSED_VARIABLE = YES; 238 | INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; 239 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 240 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 241 | MTL_ENABLE_DEBUG_INFO = NO; 242 | MTL_FAST_MATH = YES; 243 | SDKROOT = iphoneos; 244 | SWIFT_COMPILATION_MODE = wholemodule; 245 | VALIDATE_PRODUCT = YES; 246 | }; 247 | name = Release; 248 | }; 249 | 8485627C2D2C57760063928C /* Debug */ = { 250 | isa = XCBuildConfiguration; 251 | buildSettings = { 252 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 253 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 254 | CODE_SIGN_STYLE = Automatic; 255 | CURRENT_PROJECT_VERSION = 1; 256 | DEVELOPMENT_ASSET_PATHS = ""; 257 | DEVELOPMENT_TEAM = ""; 258 | ENABLE_PREVIEWS = YES; 259 | GENERATE_INFOPLIST_FILE = YES; 260 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 261 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 262 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 263 | INFOPLIST_KEY_UIRequiresFullScreen = YES; 264 | INFOPLIST_KEY_UIStatusBarHidden = YES; 265 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 266 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 267 | LD_RUNPATH_SEARCH_PATHS = ( 268 | "$(inherited)", 269 | "@executable_path/Frameworks", 270 | ); 271 | MARKETING_VERSION = 1.0; 272 | PRODUCT_BUNDLE_IDENTIFIER = com.example.Wrapper; 273 | PRODUCT_NAME = "$(TARGET_NAME)"; 274 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 275 | SUPPORTS_MACCATALYST = NO; 276 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 277 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 278 | SWIFT_EMIT_LOC_STRINGS = YES; 279 | SWIFT_VERSION = 5.0; 280 | TARGETED_DEVICE_FAMILY = 2; 281 | }; 282 | name = Debug; 283 | }; 284 | 8485627D2D2C57760063928C /* Release */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 288 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 289 | CODE_SIGN_STYLE = Automatic; 290 | CURRENT_PROJECT_VERSION = 1; 291 | DEVELOPMENT_ASSET_PATHS = ""; 292 | DEVELOPMENT_TEAM = ""; 293 | ENABLE_PREVIEWS = YES; 294 | GENERATE_INFOPLIST_FILE = YES; 295 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 296 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 297 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 298 | INFOPLIST_KEY_UIRequiresFullScreen = YES; 299 | INFOPLIST_KEY_UIStatusBarHidden = YES; 300 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 301 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 302 | LD_RUNPATH_SEARCH_PATHS = ( 303 | "$(inherited)", 304 | "@executable_path/Frameworks", 305 | ); 306 | MARKETING_VERSION = 1.0; 307 | PRODUCT_BUNDLE_IDENTIFIER = com.example.Wrapper; 308 | PRODUCT_NAME = "$(TARGET_NAME)"; 309 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 310 | SUPPORTS_MACCATALYST = NO; 311 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 312 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 313 | SWIFT_EMIT_LOC_STRINGS = YES; 314 | SWIFT_VERSION = 5.0; 315 | TARGETED_DEVICE_FAMILY = 2; 316 | }; 317 | name = Release; 318 | }; 319 | /* End XCBuildConfiguration section */ 320 | 321 | /* Begin XCConfigurationList section */ 322 | 848562682D2C57750063928C /* Build configuration list for PBXProject "Wrapper" */ = { 323 | isa = XCConfigurationList; 324 | buildConfigurations = ( 325 | 848562792D2C57760063928C /* Debug */, 326 | 8485627A2D2C57760063928C /* Release */, 327 | ); 328 | defaultConfigurationIsVisible = 0; 329 | defaultConfigurationName = Release; 330 | }; 331 | 8485627B2D2C57760063928C /* Build configuration list for PBXNativeTarget "Wrapper" */ = { 332 | isa = XCConfigurationList; 333 | buildConfigurations = ( 334 | 8485627C2D2C57760063928C /* Debug */, 335 | 8485627D2D2C57760063928C /* Release */, 336 | ); 337 | defaultConfigurationIsVisible = 0; 338 | defaultConfigurationName = Release; 339 | }; 340 | /* End XCConfigurationList section */ 341 | }; 342 | rootObject = 848562652D2C57750063928C /* Project object */; 343 | } 344 | --------------------------------------------------------------------------------