From da9653cfe82f884256767a62a0a5bcac85664744 Mon Sep 17 00:00:00 2001 From: Lei Nelissen Date: Sat, 24 May 2025 17:41:28 +0200 Subject: [PATCH] feat: base setup for carplay --- ios/AppDelegate.swift | 48 ------------------- ios/Fintunes.xcodeproj/project.pbxproj | 20 ++++++-- ios/Fintunes/AppDelegate.swift | 28 ++++++++--- ios/Fintunes/CarSceneDelegate.swift | 17 +++++++ ios/Fintunes/Fintunes-Bridging-Header.h | 1 + ios/Fintunes/Info.plist | 36 ++++++++++++++ ios/Fintunes/PhoneSceneDelegate.swift | 24 ++++++++++ ios/Podfile.lock | 8 +++- package.json | 1 + pnpm-lock.yaml | 19 ++++++++ src/components/App.tsx | 2 + src/screens/carplay/index.tsx | 62 +++++++++++++++++++++++++ 12 files changed, 207 insertions(+), 59 deletions(-) delete mode 100644 ios/AppDelegate.swift create mode 100644 ios/Fintunes/CarSceneDelegate.swift create mode 100644 ios/Fintunes/Fintunes-Bridging-Header.h create mode 100644 ios/Fintunes/PhoneSceneDelegate.swift create mode 100644 src/screens/carplay/index.tsx diff --git a/ios/AppDelegate.swift b/ios/AppDelegate.swift deleted file mode 100644 index ebc3e8f..0000000 --- a/ios/AppDelegate.swift +++ /dev/null @@ -1,48 +0,0 @@ -import UIKit -import React -import React_RCTAppDelegate -import ReactAppDependencyProvider - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - var reactNativeDelegate: ReactNativeDelegate? - var reactNativeFactory: RCTReactNativeFactory? - - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - let delegate = ReactNativeDelegate() - let factory = RCTReactNativeFactory(delegate: delegate) - delegate.dependencyProvider = RCTAppDependencyProvider() - - reactNativeDelegate = delegate - reactNativeFactory = factory - - window = UIWindow(frame: UIScreen.main.bounds) - - factory.startReactNative( - withModuleName: "Fintunes", - in: window, - launchOptions: launchOptions - ) - - return true - } -} - -class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { - override func sourceURL(for bridge: RCTBridge) -> URL? { - self.bundleURL() - } - - override func bundleURL() -> URL? { -#if DEBUG - RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") -#else - Bundle.main.url(forResource: "main", withExtension: "jsbundle") -#endif - } -} diff --git a/ios/Fintunes.xcodeproj/project.pbxproj b/ios/Fintunes.xcodeproj/project.pbxproj index 9d9096d..d2222a9 100644 --- a/ios/Fintunes.xcodeproj/project.pbxproj +++ b/ios/Fintunes.xcodeproj/project.pbxproj @@ -13,7 +13,9 @@ 4C04FC6E055249ABB204D3BC /* Inter-VariableFont_slnt,wght.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4B4A0465FF364579B28CF5D7 /* Inter-VariableFont_slnt,wght.ttf */; }; AB393FCA2857CC8400773469 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB393FC92857CC8400773469 /* SnapshotHelper.swift */; }; AB4A8DFE2857C8DA005A1ED0 /* FintunesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4A8DFD2857C8DA005A1ED0 /* FintunesUITests.swift */; }; - AB7AA5F92DC8E5D600578CAC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7AA5F82DC8E5D600578CAC /* AppDelegate.swift */; }; + ABB40BB92DE211A6002112FC /* PhoneSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB40BB82DE211A6002112FC /* PhoneSceneDelegate.swift */; }; + ABB40BBA2DE211A6002112FC /* CarSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB40BB72DE211A6002112FC /* CarSceneDelegate.swift */; }; + ABB40BBC2DE2137E002112FC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB40BBB2DE2137E002112FC /* AppDelegate.swift */; }; FA01635F2599C28FC19F2EC3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3896494129CBC30258D9BB1C /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ @@ -40,7 +42,10 @@ AB393FC92857CC8400773469 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; }; AB4A8DFB2857C8DA005A1ED0 /* FintunesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FintunesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AB4A8DFD2857C8DA005A1ED0 /* FintunesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FintunesUITests.swift; sourceTree = ""; }; - AB7AA5F82DC8E5D600578CAC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + ABB40BB42DE20F50002112FC /* Fintunes-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Fintunes-Bridging-Header.h"; path = "Fintunes/Fintunes-Bridging-Header.h"; sourceTree = ""; }; + ABB40BB72DE211A6002112FC /* CarSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CarSceneDelegate.swift; path = Fintunes/CarSceneDelegate.swift; sourceTree = ""; }; + ABB40BB82DE211A6002112FC /* PhoneSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PhoneSceneDelegate.swift; path = Fintunes/PhoneSceneDelegate.swift; sourceTree = ""; }; + ABB40BBB2DE2137E002112FC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Fintunes/AppDelegate.swift; sourceTree = ""; }; E22EC545298DA9F9017776C0 /* libPods-Fintunes.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes.a"; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; @@ -68,7 +73,10 @@ 13B07FAE1A68108700A75B9A /* Fintunes */ = { isa = PBXGroup; children = ( - AB7AA5F82DC8E5D600578CAC /* AppDelegate.swift */, + ABB40BBB2DE2137E002112FC /* AppDelegate.swift */, + ABB40BB72DE211A6002112FC /* CarSceneDelegate.swift */, + ABB40BB82DE211A6002112FC /* PhoneSceneDelegate.swift */, + ABB40BB42DE20F50002112FC /* Fintunes-Bridging-Header.h */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, @@ -348,7 +356,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AB7AA5F92DC8E5D600578CAC /* AppDelegate.swift in Sources */, + ABB40BBC2DE2137E002112FC /* AppDelegate.swift in Sources */, + ABB40BB92DE211A6002112FC /* PhoneSceneDelegate.swift in Sources */, + ABB40BBA2DE211A6002112FC /* CarSceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -414,6 +424,7 @@ PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.jellyfinaudioplayer; PRODUCT_NAME = Fintunes; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Fintunes/Fintunes-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -449,6 +460,7 @@ PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore"; + SWIFT_OBJC_BRIDGING_HEADER = "Fintunes/Fintunes-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/ios/Fintunes/AppDelegate.swift b/ios/Fintunes/AppDelegate.swift index ebc3e8f..8fe5707 100644 --- a/ios/Fintunes/AppDelegate.swift +++ b/ios/Fintunes/AppDelegate.swift @@ -1,19 +1,20 @@ import UIKit +import CarPlay import React import React_RCTAppDelegate import ReactAppDependencyProvider + @main class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? - + var reactNativeDelegate: ReactNativeDelegate? var reactNativeFactory: RCTReactNativeFactory? - - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + let delegate = ReactNativeDelegate() let factory = RCTReactNativeFactory(delegate: delegate) delegate.dependencyProvider = RCTAppDependencyProvider() @@ -31,6 +32,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + if (connectingSceneSession.role == UISceneSession.Role.carTemplateApplication) { + let scene = UISceneConfiguration(name: "CarPlay", sessionRole: connectingSceneSession.role) + scene.delegateClass = CarSceneDelegate.self + return scene + } else { + let scene = UISceneConfiguration(name: "Phone", sessionRole: connectingSceneSession.role) + scene.delegateClass = PhoneSceneDelegate.self + return scene + } + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + } } class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { diff --git a/ios/Fintunes/CarSceneDelegate.swift b/ios/Fintunes/CarSceneDelegate.swift new file mode 100644 index 0000000..17a74d4 --- /dev/null +++ b/ios/Fintunes/CarSceneDelegate.swift @@ -0,0 +1,17 @@ +import CarPlay + +class CarSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate { + func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) { + print("CarPlay: Scene did connect") + print("CarPlay: About to connect to RNCarPlay") + // Dispatch connect to RNCarPlay + RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow) + print("CarPlay: RNCarPlay.connect called") + } + + func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnect interfaceController: CPInterfaceController) { + print("CarPlay: Scene did disconnect") + // Dispatch disconnect to RNCarPlay + RNCarPlay.disconnect() + } +} \ No newline at end of file diff --git a/ios/Fintunes/Fintunes-Bridging-Header.h b/ios/Fintunes/Fintunes-Bridging-Header.h new file mode 100644 index 0000000..09f1a38 --- /dev/null +++ b/ios/Fintunes/Fintunes-Bridging-Header.h @@ -0,0 +1 @@ +#import "RNCarPlay.h" \ No newline at end of file diff --git a/ios/Fintunes/Info.plist b/ios/Fintunes/Info.plist index 7a5eafe..d5f0439 100644 --- a/ios/Fintunes/Info.plist +++ b/ios/Fintunes/Info.plist @@ -60,5 +60,41 @@ UIViewControllerBasedStatusBarAppearance + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + CPTemplateApplicationSceneSessionRoleApplication + + + UISceneClassName + CPTemplateApplicationScene + UISceneConfigurationName + CarPlay + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).CarSceneDelegate + + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + Phone + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).PhoneSceneDelegate + + + + + CPApplicationIdentifier + nl.moeilijkedingen.fintunes.carplay + CPApplicationName + Fintunes + CPApplicationCategory + CPApplicationCategoryAudio diff --git a/ios/Fintunes/PhoneSceneDelegate.swift b/ios/Fintunes/PhoneSceneDelegate.swift new file mode 100644 index 0000000..bc6be86 --- /dev/null +++ b/ios/Fintunes/PhoneSceneDelegate.swift @@ -0,0 +1,24 @@ +import UIKit +import React + +class PhoneSceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } + guard let windowScene = scene as? UIWindowScene else { return } + guard let appRootView = appDelegate.window?.rootViewController?.view else { return } + + let containerViewController = UIViewController() + containerViewController.view.addSubview(appRootView) + appRootView.frame = containerViewController.view.bounds + + let window = UIWindow(windowScene: windowScene) + window.rootViewController = containerViewController + self.window = window + window.makeKeyAndVisible() + } +} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 19e04ac..083ae08 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1399,6 +1399,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-carplay (2.4.1-beta.0): + - React - react-native-netinfo (11.4.1): - React-Core - react-native-safe-area-context (5.4.0): @@ -2310,6 +2312,7 @@ DEPENDENCIES: - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - react-native-accessibility-settings (from `../node_modules/react-native-accessibility-settings`) - "react-native-blur (from `../node_modules/@react-native-community/blur`)" + - react-native-carplay (from `../node_modules/react-native-carplay`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - "react-native-skia (from `../node_modules/@shopify/react-native-skia`)" @@ -2455,6 +2458,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-accessibility-settings" react-native-blur: :path: "../node_modules/@react-native-community/blur" + react-native-carplay: + :path: "../node_modules/react-native-carplay" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" react-native-safe-area-context: @@ -2597,6 +2602,7 @@ SPEC CHECKSUMS: React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6 react-native-accessibility-settings: 87f2276eb146ed5656bef2073e0c077e94a83f88 react-native-blur: 06d0f9906ecd6cde3a42de16c6cd829a2bf0710c + react-native-carplay: 8f388f6f73e5e0f73ed154ad8794371343ee20c0 react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 react-native-safe-area-context: 562163222d999b79a51577eda2ea8ad2c32b4d06 react-native-skia: 2f725d0747756d57f0a51417c91e33bc42272c56 @@ -2654,4 +2660,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: eb809ce42bd87a82dedb7b209e4bec32e9be4528 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/package.json b/package.json index acf4e2e..0251be9 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "react-airplay": "^1.2.0", "react-native": "^0.79.2", "react-native-accessibility-settings": "^0.1.2", + "react-native-carplay": "2.4.1-beta.0", "react-native-collapsible": "^1.6.2", "react-native-dotenv": "^3.4.11", "react-native-fs": "^2.20.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3520d01..9c844ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,6 +88,9 @@ importers: react-native-accessibility-settings: specifier: ^0.1.2 version: 0.1.2(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0) + react-native-carplay: + specifier: 2.4.1-beta.0 + version: 2.4.1-beta.0(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0) react-native-collapsible: specifier: ^1.6.2 version: 1.6.2(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0) @@ -3829,6 +3832,17 @@ packages: react: '*' react-native: '*' + react-native-carplay@2.4.1-beta.0: + resolution: {integrity: sha512-tYJymLgJi+0516niv4ApGVC+VgENX/uCYqCX81tewSILWnS6KR7M0A9+bHyNk8xoheFyYGruX7onYxU2U8ykPA==} + peerDependencies: + react: ^17.0.2 || ^18.0.0 + react-native: ^0.60.0 + peerDependenciesMeta: + react: + optional: true + react-native: + optional: true + react-native-collapsible@1.6.2: resolution: {integrity: sha512-MCOBVJWqHNjnDaGkvxX997VONmJeebh6wyJxnHEgg0L1PrlcXU1e/bo6eK+CDVFuMrCafw8Qh4DOv/C4V/+Iew==} peerDependencies: @@ -9366,6 +9380,11 @@ snapshots: react: 19.0.0 react-native: 0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0) + react-native-carplay@2.4.1-beta.0(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0): + optionalDependencies: + react: 19.0.0 + react-native: 0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0) + react-native-collapsible@1.6.2(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 diff --git a/src/components/App.tsx b/src/components/App.tsx index eac097a..3b35298 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -12,6 +12,7 @@ import { import { ColorSchemeProvider, themes, useUserOrSystemScheme } from './Colors'; import DownloadManager from './DownloadManager'; import AppLoading from './AppLoading'; +import CarPlayScreen from '@/screens/carplay'; const LightTheme = { ...DefaultTheme, @@ -84,6 +85,7 @@ export default function App(): JSX.Element | null { + diff --git a/src/screens/carplay/index.tsx b/src/screens/carplay/index.tsx new file mode 100644 index 0000000..d26841a --- /dev/null +++ b/src/screens/carplay/index.tsx @@ -0,0 +1,62 @@ +import React, { useEffect } from 'react'; +import { View, Text } from 'react-native'; +import { CarPlay, ListTemplate, ListItem } from 'react-native-carplay'; + +const CarPlayScreen: React.FC = () => { + useEffect(() => { + console.log('CarPlay: Screen mounted'); + + const onConnect = () => { + console.log('CarPlay: React Native connected'); + + // Create a list template with some items + const template = new ListTemplate({ + title: 'Fintunes', + sections: [ + { + header: 'Library', + items: [ + { + text: 'Songs', + detailText: 'Browse your music library', + }, + { + text: 'Albums', + detailText: 'Browse your albums', + }, + { + text: 'Artists', + detailText: 'Browse your artists', + }, + ], + }, + ], + onItemSelect: (item) => { + console.log('Selected item:', item); + }, + }); + + // Set the template as root + CarPlay.setRootTemplate(template); + }; + + const onDisconnect = () => { + console.log('CarPlay: React Native disconnected'); + }; + + // Register for CarPlay connection events + CarPlay.registerOnConnect(onConnect); + CarPlay.registerOnDisconnect(onDisconnect); + + return () => { + console.log('CarPlay: Screen unmounting'); + // Cleanup listeners + CarPlay.unregisterOnConnect(onConnect); + CarPlay.unregisterOnDisconnect(onDisconnect); + }; + }, []); + + return null; +}; + +export default CarPlayScreen; \ No newline at end of file