diff --git a/Pocket Poster.xcodeproj/project.pbxproj b/Pocket Poster.xcodeproj/project.pbxproj
index 634d3bf..b189ef4 100644
--- a/Pocket Poster.xcodeproj/project.pbxproj
+++ b/Pocket Poster.xcodeproj/project.pbxproj
@@ -272,7 +272,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 6;
+ CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\"";
DEVELOPMENT_TEAM = 2P4CP47H76;
ENABLE_PREVIEWS = YES;
@@ -304,7 +304,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 6;
+ CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\"";
DEVELOPMENT_TEAM = 2P4CP47H76;
ENABLE_PREVIEWS = YES;
diff --git a/Pocket Poster.xcodeproj/xcuserdata/lemin.xcuserdatad/xcschemes/xcschememanagement.plist b/Pocket Poster.xcodeproj/xcuserdata/lemin.xcuserdatad/xcschemes/xcschememanagement.plist
index d7a896f..f3372ed 100644
--- a/Pocket Poster.xcodeproj/xcuserdata/lemin.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Pocket Poster.xcodeproj/xcuserdata/lemin.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,7 +7,7 @@
Pocket Poster.xcscheme_^#shared#^_
orderHint
- 1
+ 0
diff --git a/Pocket Poster/Controllers/CarPlayWallpaper+CarPlayManager.swift b/Pocket Poster/Controllers/CarPlayWallpaper+CarPlayManager.swift
new file mode 100644
index 0000000..6a094f5
--- /dev/null
+++ b/Pocket Poster/Controllers/CarPlayWallpaper+CarPlayManager.swift
@@ -0,0 +1,149 @@
+//
+// CarPlayWallpaper.swift
+// Pocket Poster
+//
+// Created by lemin on 6/22/25.
+//
+
+import SwiftUI
+
+struct CarPlayWallpaper: Identifiable {
+ var id = UUID()
+ var name: String
+ var lightImage: UIImage
+ var darkImage: UIImage
+ var selectedImageDataLight: Data?
+ var selectedImageDataDark: Data?
+}
+
+extension CarPlayWallpaper: Reorderable {
+ typealias OrderElement = String
+ var orderElement: OrderElement { name }
+}
+
+class CarPlayManager {
+ static func supportsCarPlay() -> Bool {
+ if UIDevice.current.userInterfaceIdiom != .phone {
+ // CarPlay is not on iPads
+ return false
+ }
+ if #available(iOS 19, *) {
+ // get the build number
+ var osVersionString = [CChar](repeating: 0, count: 16)
+ var osVersionStringLen = size_t(osVersionString.count - 1)
+
+ let result = sysctlbyname("kern.osversion", &osVersionString, &osVersionStringLen, nil, 0)
+
+ if result == 0 {
+ // Convert C array to String
+ if let build = String(validatingUTF8: osVersionString) {
+ // check build number for iOS 26 dev beta 1-3, return true if user is on that
+ if build == "23A5260n" // dev beta 1
+ || build == "23A5260u" // dev beta 1 (iPhone 15/16 series)
+ || build == "23A5276f" // dev beta 2
+ || build == "23A5287g" { // dev beta 3
+ return true
+ }
+ } else {
+ print("Failed to convert build number to String")
+ }
+ } else {
+ print("sysctlbyname failed with error: \(String(cString: strerror(errno)))")
+ }
+ return false
+ }
+ return true
+ }
+
+ static func getCarPlayCacheVersion() -> String {
+ // they started adding numbers to the end in iOS 18, this is to compensate for that
+ if #available(iOS 19.0, *) {
+ return "-12"
+ } else if #available(iOS 18.0, *) {
+ return "-11"
+ }
+ return ""
+ }
+
+ static func getCarPlayWallpaperNames() -> [String]? {
+ dlopen("/System/Library/PrivateFrameworks/CarPlayUIServices.framework/CarPlayUIServices", RTLD_GLOBAL)
+
+ if #available(iOS 18.0, *) {
+ // iOS 18 method (it got moved)
+ guard let obj = objc_getClass("CRSUISystemWallpaper") as? NSObject else { print("no class"); return nil }
+
+ if let success = obj.perform(Selector(("wallpapers"))), let arr = success.takeUnretainedValue() as? [NSObject] {
+ var namesList: [String] = []
+ for wp in arr {
+ if let wpACName = wp.perform(Selector(("wallpaperAssetCatalogName"))), let result = wpACName.takeUnretainedValue() as? String {
+ namesList.append(result)
+ }
+ }
+ return namesList
+ }
+ } else {
+ // iOS 17-
+ guard let obj = objc_getClass("CRSUIWallpaperPreferences") as? NSObject else { print("no class"); return nil }
+ if let success = obj.perform(Selector(("availableWallpapers"))), let arr = success.takeUnretainedValue() as? [NSObject] {
+ var namesList: [String] = []
+ for wp in arr {
+ if let wpACName = wp.perform(Selector(("wallpaperAssetCatalogName"))), let result = wpACName.takeUnretainedValue() as? String {
+ namesList.append(result)
+ }
+ }
+ return namesList
+ }
+ }
+
+ return nil
+ }
+
+ static func getCarPlayPhotosURL() -> URL {
+ let cppURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CarPlayPhotos", conformingTo: .directory)
+ // create it if it doesn't exist
+ if !FileManager.default.fileExists(atPath: cppURL.path()) {
+ try? FileManager.default.createDirectory(at: cppURL, withIntermediateDirectories: true)
+ }
+ return cppURL
+ }
+
+ static func applyCarPlay(appHash: String, wallpapers: [CarPlayWallpaper]) throws {
+ // write the image
+ var toRemove: [URL] = []
+ var activeWP: [String] = UserDefaults.standard.array(forKey: "ActiveCarPlayWallpapers") as? [String] ?? []
+ let cppURL = getCarPlayPhotosURL()
+ let cacheVer = getCarPlayCacheVersion()
+ for wallpaper in wallpapers {
+ if let data = wallpaper.selectedImageDataLight, let img = UIImage(data: data) {
+ let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Light\(cacheVer).cpbitmap")
+ img.writeToCPBitmapFile(to: imgURL.path() as NSString)
+ try? data.write(to: cppURL.appendingPathComponent("\(wallpaper.name)-Light"))
+ toRemove.append(imgURL)
+ if !activeWP.contains(wallpaper.name) {
+ activeWP.append(wallpaper.name)
+ }
+ }
+ if let data = wallpaper.selectedImageDataDark, let img = UIImage(data: data) {
+ let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Dark\(cacheVer).cpbitmap")
+ img.writeToCPBitmapFile(to: imgURL.path() as NSString)
+ try? data.write(to: cppURL.appendingPathComponent("\(wallpaper.name)-Dark"))
+ toRemove.append(imgURL)
+ if !activeWP.contains(wallpaper.name) {
+ activeWP.append(wallpaper.name)
+ }
+ }
+ }
+
+ // symlink and apply
+ let _ = try SymHandler.createAppSymlink(for: "\(appHash)/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images")
+ defer {
+ SymHandler.cleanup()
+ }
+ for imgURL in toRemove {
+ try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
+ }
+ UserDefaults.standard.set(activeWP, forKey: "ActiveCarPlayWallpapers")
+ let test = SymHandler.getDocumentsDirectory().appendingPathComponent("Caches")
+ try Data(count: 0).write(to: test)
+ }
+}
diff --git a/Pocket Poster/Controllers/CarPlayWallpaper.swift b/Pocket Poster/Controllers/CarPlayWallpaper.swift
deleted file mode 100644
index c636249..0000000
--- a/Pocket Poster/Controllers/CarPlayWallpaper.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-//
-// CarPlayWallpaper.swift
-// Pocket Poster
-//
-// Created by lemin on 6/22/25.
-//
-
-import SwiftUI
-
-struct CarPlayWallpaper: Identifiable {
- var id = UUID()
- var name: String
- var lightImage: UIImage
- var darkImage: UIImage
- var selectedImageDataLight: Data?
- var selectedImageDataDark: Data?
-}
-
-extension CarPlayWallpaper: Reorderable {
- typealias OrderElement = String
- var orderElement: OrderElement { name }
-}
diff --git a/Pocket Poster/Controllers/PosterBoardManager.swift b/Pocket Poster/Controllers/PosterBoardManager.swift
index 7b925c1..5ee7025 100644
--- a/Pocket Poster/Controllers/PosterBoardManager.swift
+++ b/Pocket Poster/Controllers/PosterBoardManager.swift
@@ -235,98 +235,6 @@ class PosterBoardManager: ObservableObject {
}
}
- func getCarPlayCacheVersion() -> String {
- // they started adding numbers to the end in iOS 18, this is to compensate for that
- if #available(iOS 19.0, *) {
- return "-12"
- } else if #available(iOS 18.0, *) {
- return "-11"
- }
- return ""
- }
-
- func getCarPlayWallpaperNames() -> [String]? {
- dlopen("/System/Library/PrivateFrameworks/CarPlayUIServices.framework/CarPlayUIServices", RTLD_GLOBAL)
-
- if #available(iOS 18.0, *) {
- // iOS 18 method (it got moved)
- guard let obj = objc_getClass("CRSUISystemWallpaper") as? NSObject else { print("no class"); return nil }
-
- if let success = obj.perform(Selector(("wallpapers"))), let arr = success.takeUnretainedValue() as? [NSObject] {
- var namesList: [String] = []
- for wp in arr {
- if let wpACName = wp.perform(Selector(("wallpaperAssetCatalogName"))), let result = wpACName.takeUnretainedValue() as? String {
- namesList.append(result)
- }
- }
- return namesList
- }
- } else {
- // iOS 17-
- guard let obj = objc_getClass("CRSUIWallpaperPreferences") as? NSObject else { print("no class"); return nil }
- if let success = obj.perform(Selector(("availableWallpapers"))), let arr = success.takeUnretainedValue() as? [NSObject] {
- var namesList: [String] = []
- for wp in arr {
- if let wpACName = wp.perform(Selector(("wallpaperAssetCatalogName"))), let result = wpACName.takeUnretainedValue() as? String {
- namesList.append(result)
- }
- }
- return namesList
- }
- }
-
- return nil
- }
-
- func getCarPlayPhotosURL() -> URL {
- let cppURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CarPlayPhotos", conformingTo: .directory)
- // create it if it doesn't exist
- if !FileManager.default.fileExists(atPath: cppURL.path()) {
- try? FileManager.default.createDirectory(at: cppURL, withIntermediateDirectories: true)
- }
- return cppURL
- }
-
- func applyCarPlay(appHash: String, wallpapers: [CarPlayWallpaper]) throws {
- // write the image
- var toRemove: [URL] = []
- var activeWP: [String] = UserDefaults.standard.array(forKey: "ActiveCarPlayWallpapers") as? [String] ?? []
- let cppURL = getCarPlayPhotosURL()
- let cacheVer = getCarPlayCacheVersion()
- for wallpaper in wallpapers {
- if let data = wallpaper.selectedImageDataLight, let img = UIImage(data: data) {
- let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Light\(cacheVer).cpbitmap")
- img.writeToCPBitmapFile(to: imgURL.path() as NSString)
- try? data.write(to: cppURL.appendingPathComponent("\(wallpaper.name)-Light"))
- toRemove.append(imgURL)
- if !activeWP.contains(wallpaper.name) {
- activeWP.append(wallpaper.name)
- }
- }
- if let data = wallpaper.selectedImageDataDark, let img = UIImage(data: data) {
- let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Dark\(cacheVer).cpbitmap")
- img.writeToCPBitmapFile(to: imgURL.path() as NSString)
- try? data.write(to: cppURL.appendingPathComponent("\(wallpaper.name)-Dark"))
- toRemove.append(imgURL)
- if !activeWP.contains(wallpaper.name) {
- activeWP.append(wallpaper.name)
- }
- }
- }
-
- // symlink and apply
- let _ = try SymHandler.createAppSymlink(for: "\(appHash)/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images")
- defer {
- SymHandler.cleanup()
- }
- for imgURL in toRemove {
- try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
- }
- UserDefaults.standard.set(activeWP, forKey: "ActiveCarPlayWallpapers")
- let test = SymHandler.getDocumentsDirectory().appendingPathComponent("Caches")
- try Data(count: 0).write(to: test)
- }
-
static func clearCache() throws {
SymHandler.cleanup()
let docDir = SymHandler.getDocumentsDirectory()
diff --git a/Pocket Poster/Views/NavigatorViews/CarPlayView.swift b/Pocket Poster/Views/NavigatorViews/CarPlayView.swift
index 952a97f..81e4a58 100644
--- a/Pocket Poster/Views/NavigatorViews/CarPlayView.swift
+++ b/Pocket Poster/Views/NavigatorViews/CarPlayView.swift
@@ -52,7 +52,7 @@ struct CarPlayView: View {
DispatchQueue.global(qos: .userInitiated).async {
do {
- try pbManager.applyCarPlay(appHash: cpHash, wallpapers: wallpapers)
+ try CarPlayManager.applyCarPlay(appHash: cpHash, wallpapers: wallpapers)
SymHandler.cleanup() // just to be extra sure
UIApplication.shared.dismissAlert(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
@@ -103,7 +103,7 @@ struct CarPlayView: View {
// load active wallpapers from user defaults
activeWallpapers = UserDefaults.standard.array(forKey: "ActiveCarPlayWallpapers") as? [String] ?? []
// load the wallpapers
- let cppURL = PosterBoardManager.shared.getCarPlayPhotosURL()
+ let cppURL = CarPlayManager.getCarPlayPhotosURL()
let frameworkPath = "/System/Library/PrivateFrameworks/CarPlayUIServices.framework"
do {
for file in try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: frameworkPath), includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
@@ -127,7 +127,7 @@ struct CarPlayView: View {
}
}
// sort them
- if let sortedWallpapers = PosterBoardManager.shared.getCarPlayWallpaperNames() {
+ if let sortedWallpapers = CarPlayManager.getCarPlayWallpaperNames() {
wallpapers = wallpapers.reorder(by: sortedWallpapers)
}
} catch {
diff --git a/Pocket Poster/Views/RootView.swift b/Pocket Poster/Views/RootView.swift
index ff3a114..7a85a46 100644
--- a/Pocket Poster/Views/RootView.swift
+++ b/Pocket Poster/Views/RootView.swift
@@ -18,7 +18,7 @@ struct RootView: View {
.tabItem {
Label("Videos", systemImage: "camera")
}
- if UIDevice.current.userInterfaceIdiom == .phone {
+ if CarPlayManager.supportsCarPlay() {
CarPlayView()
.tabItem {
Label("CarPlay", systemImage: "car")
diff --git a/Pocket Poster/Views/SettingsView.swift b/Pocket Poster/Views/SettingsView.swift
index a7e39e8..f10275d 100644
--- a/Pocket Poster/Views/SettingsView.swift
+++ b/Pocket Poster/Views/SettingsView.swift
@@ -23,7 +23,7 @@ struct SettingsView: View {
TextField("Enter PosterBoard App Hash", text: $pbHash)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(.body, design: .monospaced))
- if UIDevice.current.userInterfaceIdiom == .phone {
+ if CarPlayManager.supportsCarPlay() {
TextField("Enter CarPlayWallpaper App Hash", text: $cpHash)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(.body, design: .monospaced))
@@ -89,7 +89,7 @@ struct SettingsView: View {
Button(action: {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
UserDefaults.standard.set(nil, forKey: "ActiveCarPlayWallpapers")
- try? FileManager.default.removeItem(at: PosterBoardManager.shared.getCarPlayPhotosURL())
+ try? FileManager.default.removeItem(at: CarPlayManager.getCarPlayPhotosURL())
Haptic.shared.notify(.success)
UIApplication.shared.alert(title: NSLocalizedString("CarPlay Applied Wallpapers Successfully Cleared!", comment: ""), body: "")
}) {