diff --git a/DemoApp/Demo/TypeSwift/TypeSwift.swift b/DemoApp/Demo/TypeSwift/TypeSwift.swift index 8c34e14..fd65ede 100644 --- a/DemoApp/Demo/TypeSwift/TypeSwift.swift +++ b/DemoApp/Demo/TypeSwift/TypeSwift.swift @@ -11,6 +11,13 @@ /// An enumeration of TypeScript identifiers generated to be used in Swift code. enum TypeSwift { + // State Variables + case total(_ value: Double) + case textFieldValue(_ value: String) + case switchValue(_ value: Bool) + case selectedDevice(_ device: Device) + case selectedOS(_ os: OperatingSystems) + // Functions case updateTotal(_ value: Double) case updateDeviceDropdown(_ device: Device) @@ -20,6 +27,13 @@ enum TypeSwift { var jsString: String { switch self { + case .total(let value): return "total.value = \(value)" + case .textFieldValue(let value): return "textFieldValue.value = `\(value)`" + case .switchValue(let value): return "switchValue.value = \(value)" + case .selectedDevice(let device): return "selectedDevice.value = Device.\(device)" + case .selectedOS(let os): return "selectedOS.value = OperatingSystems.\(os)" + + // Functions case .updateTotal(let value): return "updateTotal(\(value))" case .updateDeviceDropdown(let device): return "updateDeviceDropdown(Device.\(device))" case .updateOSDropdown(let os): return "updateOSDropdown(OperatingSystems.\(os))" @@ -43,6 +57,13 @@ import WebKit extension TypeSwift { enum MessageHandlers { + case total((Double) -> Void) + case textFieldValue((String) -> Void) + case switchValue((Bool) -> Void) + case selectedDevice((Device) -> Void) + case selectedOS((OperatingSystems) -> Void) + + // Static Functions case updateTotal((Double) -> Void) case updateTextField((String) -> Void) case updateDeviceDropdown((Device) -> Void) @@ -51,6 +72,13 @@ extension TypeSwift { var name: String { switch self { + case .total: return "total" + case .textFieldValue: return "textFieldValue" + case .switchValue: return "switchValue" + case .selectedDevice: return "selectedDevice" + case .selectedOS: return "selectedOS" + + // Static Functions case .updateTotal: return "updateTotal" case .updateTextField: return "updateTextField" case .updateDeviceDropdown: return "updateDeviceDropdown" @@ -61,18 +89,41 @@ extension TypeSwift { func handle(message: WKScriptMessage) { switch self { + case .total(let callback): + if let value = message.body as? Double { + callback(value) + } + case .textFieldValue(let callback): + if let value = message.body as? String { + callback(value) + } + case .selectedDevice(let callback): + if let deviceData = message.body as? String, + let device = Device(rawValue: deviceData) { + callback(device) + } + case .selectedOS(let callback): + if let osData = message.body as? String, + let os = OperatingSystems(rawValue: osData) { + callback(os) + } + case .switchValue(let callback): + if let value = message.body as? Bool { + callback(value) + } + + // Static Functions case .updateTotal(let callback): if let value = message.body as? Double { callback(value) } case .updateTextField(let callback): - if let text = message.body as? String { - callback(text) + if let value = message.body as? String { + callback(value) } case .updateDeviceDropdown(let callback): if let deviceData = message.body as? String, - let data = deviceData.data(using: .utf8), - let device = try? JSONDecoder().decode(Device.self, from: data) { + let device = Device(rawValue: deviceData) { callback(device) } case .updateOSDropdown(let callback): @@ -81,8 +132,8 @@ extension TypeSwift { callback(os) } case .updateSwitch(let callback): - if let switchValue = message.body as? Bool { - callback(switchValue) + if let value = message.body as? Bool { + callback(value) } } } diff --git a/DemoApp/Demo/Views/Pages/ComponentsView/ComponentsView.swift b/DemoApp/Demo/Views/Pages/ComponentsView/ComponentsView.swift index 941e586..18bf2ea 100644 --- a/DemoApp/Demo/Views/Pages/ComponentsView/ComponentsView.swift +++ b/DemoApp/Demo/Views/Pages/ComponentsView/ComponentsView.swift @@ -10,8 +10,8 @@ import SwiftUI struct ComponentsView: View { let manager: ObservableWebViewManager - @State private var textFieldValue: String = "" @State private var total: Double = 0 + @State private var textFieldValue: String = "" @State private var selectedDevice: TypeSwift.Device = .Phone @State private var selectedOS: TypeSwift.OperatingSystems = .iOS @State private var switchValue: Bool = true @@ -21,19 +21,19 @@ struct ComponentsView: View { HStack(spacing: 0) { ObservableWebView(manager: manager) .frame(width: geometry.size.width / 2) - .tsMessageHandler(.updateTotal { newValue in + .tsMessageHandler(.total { newValue in total = newValue }, manager: manager) - .tsMessageHandler(.updateTextField { newValue in + .tsMessageHandler(.textFieldValue { newValue in textFieldValue = newValue }, manager: manager) - .tsMessageHandler(.updateDeviceDropdown { newValue in + .tsMessageHandler(.selectedDevice { newValue in selectedDevice = newValue }, manager: manager) - .tsMessageHandler(.updateOSDropdown { newValue in + .tsMessageHandler(.selectedOS { newValue in selectedOS = newValue }, manager: manager) - .tsMessageHandler(.updateSwitch { newValue in + .tsMessageHandler(.switchValue { newValue in switchValue = newValue }, manager: manager) @@ -44,10 +44,10 @@ struct ComponentsView: View { ComponentSection(header: "Buttons") { HStack { PrimaryButton("+1", foreground: .white, background: .blue) { - manager.ts(.updateTotal(total + 1)) + manager.ts(.total(total + 1)) } PrimaryButton("-1", foreground: .white, background: .red) { - manager.ts(.updateTotal(total - 1)) + manager.ts(.total(total - 1)) } Text("\(total, specifier: "%.0f")") .font(.system(size: 14, weight: .medium)) @@ -57,7 +57,7 @@ struct ComponentsView: View { ComponentSection(header: "TextField") { PrimaryTextField(text: $textFieldValue) .onChange(of: textFieldValue) { - manager.ts(.updateTextField(textFieldValue)) + manager.ts(.textFieldValue(textFieldValue)) } } @@ -66,12 +66,12 @@ struct ComponentsView: View { MonoSubheader("enum") EnumDropdownMenu(selection: $selectedDevice) .onChange(of: selectedDevice) { - manager.ts(.updateDeviceDropdown(selectedDevice)) + manager.ts(.selectedDevice(selectedDevice)) } MonoSubheader("const") EnumDropdownMenu(selection: $selectedOS) .onChange(of: selectedOS) { - manager.ts(.updateOSDropdown(selectedOS)) + manager.ts(.selectedOS(selectedOS)) } } } @@ -79,7 +79,7 @@ struct ComponentsView: View { ComponentSection(header: "Switch") { LargeSwitch(state: $switchValue) .onChange(of: switchValue) { - manager.ts(.updateSwitch(switchValue)) + manager.ts(.switchValue(switchValue)) } } } diff --git a/ReactDemo/src/app/swift-components/layout.tsx b/ReactDemo/src/app/swift-components/layout.tsx index d4d2e7a..83c54d0 100644 --- a/ReactDemo/src/app/swift-components/layout.tsx +++ b/ReactDemo/src/app/swift-components/layout.tsx @@ -2,8 +2,12 @@ import { FC, ReactNode } from 'react'; const SwiftComponentsLayout: FC<{ children: ReactNode }> = ({ children }) => { return ( -
-
{children}
+
+
+

React

+

This is a React web app

+
{children}
+
); }; diff --git a/ReactDemo/src/app/swift-components/page.tsx b/ReactDemo/src/app/swift-components/page.tsx index d351833..05692e6 100644 --- a/ReactDemo/src/app/swift-components/page.tsx +++ b/ReactDemo/src/app/swift-components/page.tsx @@ -7,7 +7,7 @@ import Switch from '../../components/switches'; import ComponentSection from '../../components/sections'; import useExpose from '../../hooks/useExpose'; import useExposeType from '../../hooks/useExposeType'; -import { handlers } from '@/utils/handlers'; +import useExposeState from '@/hooks/useExposeState'; export enum Device { Phone = 'Phone', @@ -32,54 +32,19 @@ export const exposedTypes = { }; const SplitComponentsView: FC = () => { - const [total, setTotal] = useState(0); - const [selectedDevice, setSelectedDevice] = useState(); - const [selectedOS, setSelectedOS] = useState(); - const [textFieldValue, setTextFieldValue] = useState(''); - const [switchValue, setSwitchValue] = useState(false); - - useEffect(() => { - postTotal(total); - }, [total]); - - useEffect(() => { - if (selectedDevice) { - postDeviceDropdown(selectedDevice); - } - }, [selectedDevice]); - - useEffect(() => { - if (selectedOS) { - postOSDropdown(selectedOS); - } - }, [selectedOS]); - - useEffect(() => { - postSwitch(switchValue); - }, [switchValue]); - - const postTotal = (value: number) => { - if (handlers.updateTotal) { - handlers.updateTotal.postMessage(value); - } - }; - - const postTextField = (value: string) => { - handlers.updateTextField.postMessage(value); - }; - - const postDeviceDropdown = (value: Device) => { - handlers.updateDeviceDropdown.postMessage(JSON.stringify(value)); - }; - const postOSDropdown = (value: OperatingSystemType) => { - handlers.updateOSDropdown.postMessage(value); - }; - - const postSwitch = (value: boolean) => { - if (handlers.updateSwitch) { - handlers.updateSwitch.postMessage(value); - } - }; + const [total, setTotal] = useExposeState(0, 'total'); + const [selectedDevice, setSelectedDevice] = + useExposeState('selectedDevice'); + const [selectedOS, setSelectedOS] = + useExposeState('selectedOS'); + const [textFieldValue, setTextFieldValue] = useExposeState( + '', + 'textFieldValue' + ); + const [switchValue, setSwitchValue] = useExposeState( + true, + 'switchValue' + ); const handleIncrement = () => { updateTotal(total + 1); @@ -91,18 +56,15 @@ const SplitComponentsView: FC = () => { const handleDeviceSelect = (device: Device) => { setSelectedDevice(device); - postDeviceDropdown(device); }; const handleOSSelect = (os: OperatingSystemType) => { setSelectedOS(os); - postOSDropdown(os); }; const handleTextFieldChange = (event: ChangeEvent) => { const newValue = event.target.value; setTextFieldValue(newValue); - postTextField(newValue); }; const handleSwitchChange = () => { @@ -117,22 +79,18 @@ const SplitComponentsView: FC = () => { const updateDeviceDropdown = (device: Device) => { handleDeviceSelect(device); - postDeviceDropdown(device); }; const updateOSDropdown = (os: OperatingSystemType) => { handleOSSelect(os); - postOSDropdown(os); }; const updateTextField = (text: string) => { setTextFieldValue(text); - postTextField(text); }; const updateSwitch = (state: boolean) => { setSwitchValue(state); - postSwitch(state); }; useExposeType(exposedTypes); @@ -212,16 +170,4 @@ const OperatingSystemDropdown: FC<{ ); }; -const SwiftComponents: FC = () => { - return ( -
-
-

React

-

This is a React web app

- -
-
- ); -}; - -export default SwiftComponents; +export default SplitComponentsView; diff --git a/ReactDemo/src/hooks/useExposeState.ts b/ReactDemo/src/hooks/useExposeState.ts new file mode 100644 index 0000000..0dfeb42 --- /dev/null +++ b/ReactDemo/src/hooks/useExposeState.ts @@ -0,0 +1,73 @@ +import { + useState, + useEffect, + useMemo, + useRef, + Dispatch, + SetStateAction, +} from 'react'; + +function useExposeState( + key: string +): [T | undefined, Dispatch>]; +function useExposeState( + initialValue: T, + key: string +): [T, Dispatch>]; + +function useExposeState(...args: [string] | [T, string]) { + let initialValue: T | undefined; + let key: string; + + if (args.length === 1) { + initialValue = undefined; + key = args[0]; + } else { + initialValue = args[0]; + key = args[1]; + } + + const [state, _setState] = useState(initialValue); + + const stateRef = useRef(state); + stateRef.current = state; + + const handler = useMemo( + () => ({ + set: (obj: any, prop: string, value: T | undefined) => { + if (prop === 'value') { + _setState(value); + if (window.webkit?.messageHandlers?.[key]) { + window.webkit.messageHandlers[key].postMessage(value); + } else { + console.warn(`Message handler '${key}' is not available.`); + } + } + return true; + }, + }), + [key] + ); + + const stateProxy = useMemo( + () => new Proxy({ value: stateRef.current }, handler), + [stateRef, handler] + ); + + useEffect(() => { + (window as any)[key] = stateProxy; + }, [key, stateProxy]); + + const setState: Dispatch> = (newValue) => { + if (typeof newValue === 'function') { + const valueFn = newValue as (prevState: T | undefined) => T | undefined; + stateProxy.value = valueFn(stateRef.current); + } else { + stateProxy.value = newValue; + } + }; + + return [state, setState]; +} + +export default useExposeState; diff --git a/ReactDemo/src/types/global.d.ts b/ReactDemo/src/types/global.d.ts index 165117a..0600c84 100644 --- a/ReactDemo/src/types/global.d.ts +++ b/ReactDemo/src/types/global.d.ts @@ -4,6 +4,15 @@ export {}; export type AllHandlers = ComponentHandlers; +declare global { + interface Window { + webkit: { + messageHandlers; + }; + } +} + +/* declare global { interface Window { webkit: { @@ -11,3 +20,4 @@ declare global { }; } } +*/