diff --git a/packages/apple-targets/src/target.ts b/packages/apple-targets/src/target.ts index 521153e..8461aa4 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" + | "custom-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": "custom-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, + "custom-keyboard": true, }; // TODO: Maybe we can replace `NSExtensionPrincipalClass` with the `@main` annotation that newer extensions use? @@ -279,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 @@ -313,6 +327,7 @@ export function needsEmbeddedSwift(type: ExtensionType) { "quicklook-thumbnail", "matter", "clip", + "custom-keyboard", ].includes(type); } diff --git a/packages/create-target/src/createAsync.ts b/packages/create-target/src/createAsync.ts index bcdf9dd..81898d9 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", + "custom-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..5391d5e 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: "custom-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..463f828 --- /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: "custom-keyboard", + icon: 'https://github.com/expo.png', + entitlements: { /* Add entitlements */ }, +}); \ No newline at end of file