From 50426aa53d0b262f796e6ce577ffaaa3c117fd3f Mon Sep 17 00:00:00 2001 From: export-mike Date: Mon, 16 Dec 2024 17:47:06 +1100 Subject: [PATCH 1/4] keyboard target wip --- app.json | 5 ++ packages/apple-targets/src/target.ts | 6 +- packages/create-target/src/createAsync.ts | 4 + packages/create-target/src/promptTarget.ts | 1 + .../templates/keyboard/Info.plist | 24 ++++++ .../keyboard/KeyboardViewController.swift | 85 +++++++++++++++++++ targets/keyboard/Info.plist | 11 +++ targets/keyboard/KeyboardViewController.swift | 85 +++++++++++++++++++ targets/keyboard/expo-target.config.js | 6 ++ 9 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 app.json create mode 100644 packages/create-target/templates/keyboard/Info.plist create mode 100644 packages/create-target/templates/keyboard/KeyboardViewController.swift create mode 100644 targets/keyboard/Info.plist create mode 100644 targets/keyboard/KeyboardViewController.swift create mode 100644 targets/keyboard/expo-target.config.js diff --git a/app.json b/app.json new file mode 100644 index 0000000..c38d4e5 --- /dev/null +++ b/app.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "@bacons/apple-targets" + ] +} diff --git a/packages/apple-targets/src/target.ts b/packages/apple-targets/src/target.ts index 521153e..f406386 100644 --- a/packages/apple-targets/src/target.ts +++ b/packages/apple-targets/src/target.ts @@ -21,7 +21,8 @@ export type ExtensionType = | "action" | "safari" | "app-intent" - | "device-activity-monitor"; + | "device-activity-monitor" + | "keyboard"; export const KNOWN_EXTENSION_POINT_IDENTIFIERS: Record = { @@ -46,6 +47,7 @@ export const KNOWN_EXTENSION_POINT_IDENTIFIERS: Record = "com.apple.appintents-extension": "app-intent", "com.apple.deviceactivity.monitor-extension": "device-activity-monitor", // "com.apple.intents-service": "intents", + "com.apple.keyboard-service": "keyboard", }; // An exhaustive list of extension types that should sync app groups from the main target by default when @@ -72,6 +74,7 @@ export const SHOULD_USE_APP_GROUPS_BY_DEFAULT: Record = safari: false, spotlight: false, watch: false, + keyboard: true, }; // TODO: Maybe we can replace `NSExtensionPrincipalClass` with the `@main` annotation that newer extensions use? @@ -313,6 +316,7 @@ export function needsEmbeddedSwift(type: ExtensionType) { "quicklook-thumbnail", "matter", "clip", + "keyboard", ].includes(type); } diff --git a/packages/create-target/src/createAsync.ts b/packages/create-target/src/createAsync.ts index bcdf9dd..a4b03df 100644 --- a/packages/create-target/src/createAsync.ts +++ b/packages/create-target/src/createAsync.ts @@ -190,6 +190,7 @@ export function getTemplateConfig(target: string) { "safari", "share", "watch", + "keyboard", ].includes(target); const lines = [ @@ -256,4 +257,7 @@ const RECOMMENDED_ENTITLEMENTS: Record, any> = { "device-activity-monitor": { "com.apple.developer.family-controls": true, }, + "sandbox": { + "com.apple.security.app-sandbox": true + } }; diff --git a/packages/create-target/src/promptTarget.ts b/packages/create-target/src/promptTarget.ts index b4e68a0..c12ed69 100644 --- a/packages/create-target/src/promptTarget.ts +++ b/packages/create-target/src/promptTarget.ts @@ -58,6 +58,7 @@ export const TARGETS = [ { title: "Siri Intent UI", value: "intent-ui", description: "" }, { title: "Share Extension", value: "share", description: "" }, { title: "Watch", value: "watch", description: "" }, + { title: "Keyboard", value: "keyboard", description: "" }, ]; export function assertValidTarget(target: any): asserts target is string { diff --git a/packages/create-target/templates/keyboard/Info.plist b/packages/create-target/templates/keyboard/Info.plist new file mode 100644 index 0000000..531b1b8 --- /dev/null +++ b/packages/create-target/templates/keyboard/Info.plist @@ -0,0 +1,24 @@ + + + + + NSExtension + + NSExtensionAttributes + + IsASCIICapable + + PrefersRightToLeft + + PrimaryLanguage + en-US + RequestsOpenAccess + + + NSExtensionPointIdentifier + com.apple.keyboard-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).KeyboardViewController + + + diff --git a/packages/create-target/templates/keyboard/KeyboardViewController.swift b/packages/create-target/templates/keyboard/KeyboardViewController.swift new file mode 100644 index 0000000..1edbc0b --- /dev/null +++ b/packages/create-target/templates/keyboard/KeyboardViewController.swift @@ -0,0 +1,85 @@ +import UIKit +import SwiftUI + +class KeyboardViewController: UIInputViewController { + + private var keyboardView: UIView? + + override func viewDidLoad() { + super.viewDidLoad() + + // Setup the custom keyboard UI + setupKeyboardUI() + } + + private func setupKeyboardUI() { + // Create and setup the SwiftUI keyboard view + let customKeyboardView = CustomKeyboardView(viewController: self) + let hostingController = UIHostingController(rootView: customKeyboardView) + + addChild(hostingController) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingController.view) + + NSLayoutConstraint.activate([ + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + hostingController.didMove(toParent: self) + } +} + +struct CustomKeyboardView: View { + weak var viewController: UIInputViewController? + + init(viewController: UIInputViewController) { + self.viewController = viewController + } + + var body: some View { + VStack(spacing: 10) { + HStack(spacing: 5) { + KeyButton(text: "Q") { insertText("Q") } + KeyButton(text: "W") { insertText("W") } + KeyButton(text: "E") { insertText("E") } + KeyButton(text: "R") { insertText("R") } + KeyButton(text: "T") { insertText("T") } + } + // Add more rows of keys as needed + + HStack { + KeyButton(text: "Space") { insertText(" ") } + .frame(maxWidth: .infinity) + KeyButton(text: "⌫") { deleteBackward() } + } + } + .padding() + .background(Color(.systemBackground)) + } + + private func insertText(_ text: String) { + viewController?.textDocumentProxy.insertText(text) + } + + private func deleteBackward() { + viewController?.textDocumentProxy.deleteBackward() + } +} + +struct KeyButton: View { + let text: String + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(text) + .font(.system(size: 20)) + .frame(minWidth: 30, minHeight: 40) + .background(Color(.systemGray5)) + .cornerRadius(5) + } + } +} \ No newline at end of file diff --git a/targets/keyboard/Info.plist b/targets/keyboard/Info.plist new file mode 100644 index 0000000..47be322 --- /dev/null +++ b/targets/keyboard/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.keyboard-service + + + \ No newline at end of file diff --git a/targets/keyboard/KeyboardViewController.swift b/targets/keyboard/KeyboardViewController.swift new file mode 100644 index 0000000..1edbc0b --- /dev/null +++ b/targets/keyboard/KeyboardViewController.swift @@ -0,0 +1,85 @@ +import UIKit +import SwiftUI + +class KeyboardViewController: UIInputViewController { + + private var keyboardView: UIView? + + override func viewDidLoad() { + super.viewDidLoad() + + // Setup the custom keyboard UI + setupKeyboardUI() + } + + private func setupKeyboardUI() { + // Create and setup the SwiftUI keyboard view + let customKeyboardView = CustomKeyboardView(viewController: self) + let hostingController = UIHostingController(rootView: customKeyboardView) + + addChild(hostingController) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingController.view) + + NSLayoutConstraint.activate([ + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + hostingController.didMove(toParent: self) + } +} + +struct CustomKeyboardView: View { + weak var viewController: UIInputViewController? + + init(viewController: UIInputViewController) { + self.viewController = viewController + } + + var body: some View { + VStack(spacing: 10) { + HStack(spacing: 5) { + KeyButton(text: "Q") { insertText("Q") } + KeyButton(text: "W") { insertText("W") } + KeyButton(text: "E") { insertText("E") } + KeyButton(text: "R") { insertText("R") } + KeyButton(text: "T") { insertText("T") } + } + // Add more rows of keys as needed + + HStack { + KeyButton(text: "Space") { insertText(" ") } + .frame(maxWidth: .infinity) + KeyButton(text: "⌫") { deleteBackward() } + } + } + .padding() + .background(Color(.systemBackground)) + } + + private func insertText(_ text: String) { + viewController?.textDocumentProxy.insertText(text) + } + + private func deleteBackward() { + viewController?.textDocumentProxy.deleteBackward() + } +} + +struct KeyButton: View { + let text: String + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(text) + .font(.system(size: 20)) + .frame(minWidth: 30, minHeight: 40) + .background(Color(.systemGray5)) + .cornerRadius(5) + } + } +} \ No newline at end of file diff --git a/targets/keyboard/expo-target.config.js b/targets/keyboard/expo-target.config.js new file mode 100644 index 0000000..5b797de --- /dev/null +++ b/targets/keyboard/expo-target.config.js @@ -0,0 +1,6 @@ +/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */ +module.exports = config => ({ + type: "keyboard", + icon: 'https://github.com/expo.png', + entitlements: { /* Add entitlements */ }, +}); \ No newline at end of file From f8f807dc9c265770105da33d6e5c7b170f7012b1 Mon Sep 17 00:00:00 2001 From: export-mike Date: Mon, 16 Dec 2024 17:54:53 +1100 Subject: [PATCH 2/4] remove file --- app.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 app.json diff --git a/app.json b/app.json deleted file mode 100644 index c38d4e5..0000000 --- a/app.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "plugins": [ - "@bacons/apple-targets" - ] -} From 757b7481599003d255fa65405a0ba0c13c531c15 Mon Sep 17 00:00:00 2001 From: export-mike Date: Mon, 16 Dec 2024 18:00:27 +1100 Subject: [PATCH 3/4] rename --- packages/apple-targets/src/target.ts | 8 ++++---- packages/create-target/src/createAsync.ts | 8 ++++---- packages/create-target/src/promptTarget.ts | 2 +- targets/keyboard/expo-target.config.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/apple-targets/src/target.ts b/packages/apple-targets/src/target.ts index f406386..dbfec2c 100644 --- a/packages/apple-targets/src/target.ts +++ b/packages/apple-targets/src/target.ts @@ -21,8 +21,8 @@ export type ExtensionType = | "action" | "safari" | "app-intent" - | "device-activity-monitor" - | "keyboard"; + | "device-activity-monitor" + | "custom-keyboard"; export const KNOWN_EXTENSION_POINT_IDENTIFIERS: Record = { @@ -47,7 +47,7 @@ export const KNOWN_EXTENSION_POINT_IDENTIFIERS: Record = "com.apple.appintents-extension": "app-intent", "com.apple.deviceactivity.monitor-extension": "device-activity-monitor", // "com.apple.intents-service": "intents", - "com.apple.keyboard-service": "keyboard", + "com.apple.keyboard-service": "custom-keyboard", }; // An exhaustive list of extension types that should sync app groups from the main target by default when @@ -316,7 +316,7 @@ export function needsEmbeddedSwift(type: ExtensionType) { "quicklook-thumbnail", "matter", "clip", - "keyboard", + "custom-keyboard", ].includes(type); } diff --git a/packages/create-target/src/createAsync.ts b/packages/create-target/src/createAsync.ts index a4b03df..81898d9 100644 --- a/packages/create-target/src/createAsync.ts +++ b/packages/create-target/src/createAsync.ts @@ -190,7 +190,7 @@ export function getTemplateConfig(target: string) { "safari", "share", "watch", - "keyboard", + "custom-keyboard", ].includes(target); const lines = [ @@ -257,7 +257,7 @@ const RECOMMENDED_ENTITLEMENTS: Record, any> = { "device-activity-monitor": { "com.apple.developer.family-controls": true, }, - "sandbox": { - "com.apple.security.app-sandbox": true - } + sandbox: { + "com.apple.security.app-sandbox": true, + }, }; diff --git a/packages/create-target/src/promptTarget.ts b/packages/create-target/src/promptTarget.ts index c12ed69..5391d5e 100644 --- a/packages/create-target/src/promptTarget.ts +++ b/packages/create-target/src/promptTarget.ts @@ -58,7 +58,7 @@ export const TARGETS = [ { title: "Siri Intent UI", value: "intent-ui", description: "" }, { title: "Share Extension", value: "share", description: "" }, { title: "Watch", value: "watch", description: "" }, - { title: "Keyboard", value: "keyboard", description: "" }, + { title: "Keyboard", value: "custom-keyboard", description: "" }, ]; export function assertValidTarget(target: any): asserts target is string { diff --git a/targets/keyboard/expo-target.config.js b/targets/keyboard/expo-target.config.js index 5b797de..463f828 100644 --- a/targets/keyboard/expo-target.config.js +++ b/targets/keyboard/expo-target.config.js @@ -1,6 +1,6 @@ /** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */ module.exports = config => ({ - type: "keyboard", + type: "custom-keyboard", icon: 'https://github.com/expo.png', entitlements: { /* Add entitlements */ }, }); \ No newline at end of file From 9eb0fd3a8579d5150fbe8c4de4e9fd0e5f6b4635 Mon Sep 17 00:00:00 2001 From: export-mike Date: Mon, 16 Dec 2024 18:01:48 +1100 Subject: [PATCH 4/4] plist --- packages/apple-targets/src/target.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/apple-targets/src/target.ts b/packages/apple-targets/src/target.ts index dbfec2c..8461aa4 100644 --- a/packages/apple-targets/src/target.ts +++ b/packages/apple-targets/src/target.ts @@ -74,7 +74,7 @@ export const SHOULD_USE_APP_GROUPS_BY_DEFAULT: Record = safari: false, spotlight: false, watch: false, - keyboard: true, + "custom-keyboard": true, }; // TODO: Maybe we can replace `NSExtensionPrincipalClass` with the `@main` annotation that newer extensions use? @@ -282,6 +282,17 @@ export function getTargetInfoPlistForType(type: ExtensionType) { NSExtensionPointIdentifier, }, }); + } else if (type === "custom-keyboard") { + return plist.build({ + NSExtension: { + NSExtensionAttributes: { + RequestsOpenAccess: true, + }, + NSExtensionPointIdentifier, + NSExtensionPrincipalClass: + "$(PRODUCT_MODULE_NAME).KeyboardViewController", + }, + }); } // Default: used for widget and bg-download