@ondeinference / klepon / commits / 9205c78

Add iPad app icon sizes and support all iPad orientations

- Add iPad icon assets and update Contents.json for iPad sizes - Support all interface orientations on iPad in Info.plist and project.yml - Remove forced light mode on iPad; use system appearance - Refactor AppRootView for iPad split view and platform-specific sheets - Add OndePrivacyInfo.xcprivacy privacy manifest - Update entitlements for increased memory limit on macOS

Seto Elkahfi committed May 15, 2026 at 23:23 UTC 9205c789d9f84b7c6f0d73843f12ca281b28394d
17 files changed +440 -60
Klepon.xcodeproj/project.pbxproj
+57 -15
@@ -212,14 +212,14 @@
212 4C77AF5EB4B17D087FA522F6 /* WatchContentLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchContentLoader.swift; sourceTree = "<group>"; };
213 52DD909BB2EF68103CB46EDA /* KleponActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KleponActionButton.swift; sourceTree = "<group>"; };
214 5AF291E9C2E66209377138E6 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
215 - 5DE91827A635C0B0B72690EC /* Klepon.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Klepon.app; sourceTree = BUILT_PRODUCTS_DIR; };
215 + 5DE91827A635C0B0B72690EC /* Klepon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Klepon.app; sourceTree = BUILT_PRODUCTS_DIR; };
216 6235AD6792F25476E2A508F0 /* SearchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchService.swift; sourceTree = "<group>"; };
217 643D52BCEDF66CC8FE8A1234 /* RecentSearchStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentSearchStore.swift; sourceTree = "<group>"; };
218 67FAA69C42F1845ECB79AC16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
219 6B67BCC5A459288FB240C6E7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
220 6EFBF98118F066D3C7FA14B2 /* KleponWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KleponWatchApp.swift; sourceTree = "<group>"; };
221 7541983FF7CE190C0663294D /* GuideDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideDetailView.swift; sourceTree = "<group>"; };
222 - 754A7FAB8C92112B8B6228B7 /* Klepon TV.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "Klepon TV.app"; sourceTree = BUILT_PRODUCTS_DIR; };
222 + 754A7FAB8C92112B8B6228B7 /* Klepon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Klepon.app; sourceTree = BUILT_PRODUCTS_DIR; };
223 759E92BBAF958F5882F17C37 /* KleponSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KleponSnapshotTests.swift; sourceTree = "<group>"; };
224 7F2850226B4B05FF758C1F93 /* GuideArtworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideArtworkView.swift; sourceTree = "<group>"; };
225 85E7B5E3D9A1906C2FED3032 /* EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = "<group>"; };
@@ -230,7 +230,7 @@
230 9D6EFA021D4524508DA8E78C /* testGuideArtworkView.guide-artwork.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testGuideArtworkView.guide-artwork.png"; sourceTree = "<group>"; };
231 9E76510CC840CE068B0FC381 /* Klepon Watch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Klepon Watch.app"; sourceTree = BUILT_PRODUCTS_DIR; };
232 A4400AB0AB8D245325A00BA3 /* Klepon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Klepon.entitlements; sourceTree = "<group>"; };
233 - A48AC0E39277DEFCE5D103CC /* KleponSnapshotTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = KleponSnapshotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
233 + A48AC0E39277DEFCE5D103CC /* KleponSnapshotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KleponSnapshotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
234 A7E1C50BFBE2EF5F37941590 /* KleponMetadataRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KleponMetadataRow.swift; sourceTree = "<group>"; };
235 AF79D8DD27B9CA485140C934 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
236 B02B0EA4B8BA09604D62A62B /* AnswerCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnswerCard.swift; sourceTree = "<group>"; };
@@ -240,9 +240,9 @@
240 C675ABBBA25BBA25A8DE29BE /* KleponColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KleponColor.swift; sourceTree = "<group>"; };
241 C82874C90A5FAA32D1B52EB7 /* AppRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootView.swift; sourceTree = "<group>"; };
242 CD520C870387E82E9A1533C2 /* KleponTypography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KleponTypography.swift; sourceTree = "<group>"; };
243 - D53EA049112C7D59177F9878 /* Klepon Mac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Klepon Mac.app"; sourceTree = BUILT_PRODUCTS_DIR; };
243 + D53EA049112C7D59177F9878 /* Klepon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Klepon.app; sourceTree = BUILT_PRODUCTS_DIR; };
244 DEDE2A1E73EB15970A17A894 /* KleponApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KleponApp.swift; sourceTree = "<group>"; };
245 - E041DB1D0DAA58746AE6F9EA /* Klepon Vision.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "Klepon Vision.app"; sourceTree = BUILT_PRODUCTS_DIR; };
245 + E041DB1D0DAA58746AE6F9EA /* Klepon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Klepon.app; sourceTree = BUILT_PRODUCTS_DIR; };
246 E45070309D2C7058BAA44CD2 /* RecentlyViewedStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyViewedStore.swift; sourceTree = "<group>"; };
247 E7E886BAB66E828950DBED52 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
248 EDBB4F5D4C98D036AC52E193 /* WatchHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeView.swift; sourceTree = "<group>"; };
@@ -325,9 +325,9 @@
325 3F9369D339C14126AB44D720 /* Products */ = {
326 isa = PBXGroup;
327 children = (
328 - D53EA049112C7D59177F9878 /* Klepon Mac.app */,
329 - 754A7FAB8C92112B8B6228B7 /* Klepon TV.app */,
330 - E041DB1D0DAA58746AE6F9EA /* Klepon Vision.app */,
328 + D53EA049112C7D59177F9878 /* Klepon.app */,
329 + 754A7FAB8C92112B8B6228B7 /* Klepon.app */,
330 + E041DB1D0DAA58746AE6F9EA /* Klepon.app */,
331 9E76510CC840CE068B0FC381 /* Klepon Watch.app */,
332 5DE91827A635C0B0B72690EC /* Klepon.app */,
333 A48AC0E39277DEFCE5D103CC /* KleponSnapshotTests.xctest */,
@@ -563,13 +563,14 @@
563 CECDF5959E6652B49DCF1E18 /* Onde */,
564 );
565 productName = "Klepon Vision";
566 - productReference = E041DB1D0DAA58746AE6F9EA /* Klepon Vision.app */;
566 + productReference = E041DB1D0DAA58746AE6F9EA /* Klepon.app */;
567 productType = "com.apple.product-type.application";
568 };
569 2ADB11F6C1D51B9F9A9DB15F /* Klepon */ = {
570 isa = PBXNativeTarget;
571 buildConfigurationList = E1E5A3DF58C9552EE814CC23 /* Build configuration list for PBXNativeTarget "Klepon" */;
572 buildPhases = (
573 + 923C71812D16201CD8F465F5 /* Inject PrivacyInfo into OndeFramework */,
574 142193A63A26ACC180E1EB37 /* Sources */,
575 BC5A6BBAFB4A1B61000065D1 /* Resources */,
576 F72797E506E8FA89264A5C68 /* Frameworks */,
@@ -593,6 +594,7 @@
594 isa = PBXNativeTarget;
595 buildConfigurationList = 78899F7C62A9675700D42C5A /* Build configuration list for PBXNativeTarget "Klepon Mac" */;
596 buildPhases = (
597 + D33E59FA248341330B8D1DBC /* Inject PrivacyInfo into OndeFramework */,
598 625CD4834F7E62F4BCE58404 /* Sources */,
599 667705EEBEDEBBCA43E94EAC /* Resources */,
600 B0052635A882F0E996B81154 /* Frameworks */,
@@ -607,7 +609,7 @@
609 35BDA28E3F43D5441C5FE932 /* Onde */,
610 );
611 productName = "Klepon Mac";
610 - productReference = D53EA049112C7D59177F9878 /* Klepon Mac.app */;
612 + productReference = D53EA049112C7D59177F9878 /* Klepon.app */;
613 productType = "com.apple.product-type.application";
614 };
615 E153DCB2727ADB891CF09A25 /* Klepon TV */ = {
@@ -628,7 +630,7 @@
630 D977F065ACEF38E106A8FB5C /* Onde */,
631 );
632 productName = "Klepon TV";
631 - productReference = 754A7FAB8C92112B8B6228B7 /* Klepon TV.app */;
633 + productReference = 754A7FAB8C92112B8B6228B7 /* Klepon.app */;
634 productType = "com.apple.product-type.application";
635 };
636 E8CEBF52617721AEE450A7CF /* Klepon Watch */ = {
@@ -892,6 +894,46 @@
894 shellScript = "set -eu\n\nSHARED_BUNDLE_VERSION_FILE=\"${BUILD_DIR}/klepon-cfbundleversion.txt\"\nif [ -f \"${SHARED_BUNDLE_VERSION_FILE}\" ]; then\n BUNDLE_VERSION=\"$(cat \"${SHARED_BUNDLE_VERSION_FILE}\")\"\nelse\n BUNDLE_VERSION=\"$(date -u +\"%Y%m%d.%H%M\")\"\n printf '%s' \"${BUNDLE_VERSION}\" > \"${SHARED_BUNDLE_VERSION_FILE}\"\nfi\n\nINFO_PLIST=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n\nif [ ! -f \"${INFO_PLIST}\" ]; then\n echo \"error: Info.plist not found at ${INFO_PLIST}\"\n exit 1\nfi\n\nif ! /usr/libexec/PlistBuddy -c \"Set :CFBundleVersion ${BUNDLE_VERSION}\" \"${INFO_PLIST}\" >/dev/null 2>&1; then\n /usr/libexec/PlistBuddy -c \"Add :CFBundleVersion string ${BUNDLE_VERSION}\" \"${INFO_PLIST}\"\nfi\n\necho \"Updated CFBundleVersion to ${BUNDLE_VERSION}\"\n";
895 showEnvVarsInLog = 0;
896 };
897 + 923C71812D16201CD8F465F5 /* Inject PrivacyInfo into OndeFramework */ = {
898 + isa = PBXShellScriptBuildPhase;
899 + alwaysOutOfDate = 1;
900 + buildActionMask = 2147483647;
901 + files = (
902 + );
903 + inputFileListPaths = (
904 + );
905 + inputPaths = (
906 + );
907 + name = "Inject PrivacyInfo into OndeFramework";
908 + outputFileListPaths = (
909 + );
910 + outputPaths = (
911 + );
912 + runOnlyForDeploymentPostprocessing = 0;
913 + shellPath = /bin/sh;
914 + shellScript = "set -eu\n# PrivacyInfo.xcprivacy is required inside every XCFramework slice.\n# Apple's scanner does not accept the app-level manifest as a substitute\n# for manifests inside nested third-party framework bundles.\n# BUILD_DIR is .../DerivedData/<name>/Build/Intermediates.noindex\n# SourcePackages is always two levels up from BUILD_DIR.\nXCFW=\"${BUILD_DIR}/../../SourcePackages/artifacts/onde-swift/OndeFramework/OndeFramework.xcframework\"\nPRIVACY_SRC=\"${SRCROOT}/Scripts/OndePrivacyInfo.xcprivacy\"\nif [ ! -d \"${XCFW}\" ]; then\n echo \"warning: OndeFramework.xcframework not found at ${XCFW} — skipping PrivacyInfo injection\"\n exit 0\nfi\nfor SLICE in \"${XCFW}\"/*/; do\n DEST=\"${SLICE}PrivacyInfo.xcprivacy\"\n if [ ! -f \"${DEST}\" ]; then\n cp \"${PRIVACY_SRC}\" \"${DEST}\"\n echo \"Injected PrivacyInfo.xcprivacy into ${SLICE}\"\n fi\ndone\n";
915 + showEnvVarsInLog = 0;
916 + };
917 + D33E59FA248341330B8D1DBC /* Inject PrivacyInfo into OndeFramework */ = {
918 + isa = PBXShellScriptBuildPhase;
919 + alwaysOutOfDate = 1;
920 + buildActionMask = 2147483647;
921 + files = (
922 + );
923 + inputFileListPaths = (
924 + );
925 + inputPaths = (
926 + );
927 + name = "Inject PrivacyInfo into OndeFramework";
928 + outputFileListPaths = (
929 + );
930 + outputPaths = (
931 + );
932 + runOnlyForDeploymentPostprocessing = 0;
933 + shellPath = /bin/sh;
934 + shellScript = "set -eu\nXCFW=\"${BUILD_DIR}/../../SourcePackages/artifacts/onde-swift/OndeFramework/OndeFramework.xcframework\"\nPRIVACY_SRC=\"${SRCROOT}/Scripts/OndePrivacyInfo.xcprivacy\"\nif [ ! -d \"${XCFW}\" ]; then\n echo \"warning: OndeFramework.xcframework not found at ${XCFW} — skipping PrivacyInfo injection\"\n exit 0\nfi\nfor SLICE in \"${XCFW}\"/*/; do\n DEST=\"${SLICE}PrivacyInfo.xcprivacy\"\n if [ ! -f \"${DEST}\" ]; then\n cp \"${PRIVACY_SRC}\" \"${DEST}\"\n echo \"Injected PrivacyInfo.xcprivacy into ${SLICE}\"\n fi\ndone\n";
935 + showEnvVarsInLog = 0;
936 + };
937 /* End PBXShellScriptBuildPhase section */
938
939 /* Begin PBXSourcesBuildPhase section */
@@ -1300,11 +1342,11 @@
1342 "$(inherited)",
1343 "@executable_path/Frameworks",
1344 );
1303 - MARKETING_VERSION = 1.0.0;
1345 + MARKETING_VERSION = 1.0.1;
1346 PRODUCT_BUNDLE_IDENTIFIER = ai.splitfire.klepon;
1347 SDKROOT = iphoneos;
1348 SWIFT_OPTIMIZATION_LEVEL = "-O";
1307 - TARGETED_DEVICE_FAMILY = 1;
1349 + TARGETED_DEVICE_FAMILY = "1,2";
1350 };
1351 name = Release;
1352 };
@@ -1465,11 +1507,11 @@
1507 "$(inherited)",
1508 "@executable_path/Frameworks",
1509 );
1468 - MARKETING_VERSION = 1.0.0;
1510 + MARKETING_VERSION = 1.0.1;
1511 PRODUCT_BUNDLE_IDENTIFIER = ai.splitfire.klepon;
1512 SDKROOT = iphoneos;
1513 SWIFT_OPTIMIZATION_LEVEL = "-Onone";
1472 - TARGETED_DEVICE_FAMILY = 1;
1514 + TARGETED_DEVICE_FAMILY = "1,2";
1515 };
1516 name = Debug;
1517 };
Klepon/App/AppRootView.swift
+113 -25
@@ -1,7 +1,7 @@
1 import SwiftUI
2
3 struct AppRootView: View {
4 - private enum Tab {
4 + private enum Tab: Hashable {
5 case discover
6 case search
7 case saved
@@ -16,39 +16,53 @@ struct AppRootView: View {
16 @State private var showingSettings = false
17 @State private var showingOnboarding = false
18
19 + // iPad / macOS split-view sidebar selection
20 + @State private var sidebarSelection: Tab? = .discover
21 +
22 + #if os(iOS)
23 + @Environment(\.horizontalSizeClass) private var horizontalSizeClass
24 + private var isRegularWidth: Bool { horizontalSizeClass == .regular }
25 + #endif
26 +
27 var body: some View {
28 + #if os(iOS)
29 + if isRegularWidth {
30 + iPadRootView
31 + } else {
32 + iPhoneRootView
33 + }
34 + #else
35 + iPhoneRootView
36 + #endif
37 + }
38 +
39 + // MARK: – iPhone / tvOS root (TabView)
40 +
41 + private var iPhoneRootView: some View {
42 TabView(selection: $selectedTab) {
43 NavigationStack {
44 DiscoverView(showingSettings: $showingSettings)
45 }
24 - .tabItem {
25 - Label("Discover", systemImage: "sparkles")
26 - }
46 + .tabItem { Label("Discover", systemImage: "sparkles") }
47 .tag(Tab.discover)
48
49 NavigationStack {
50 SearchView()
51 }
32 - .tabItem {
33 - Label("Search", systemImage: "magnifyingglass")
34 - }
52 + .tabItem { Label("Search", systemImage: "magnifyingglass") }
53 .tag(Tab.search)
54
55 NavigationStack {
56 SavedView()
57 }
40 - .tabItem {
41 - Label("Saved", systemImage: "heart")
42 - }
58 + .tabItem { Label("Saved", systemImage: "heart") }
59 .tag(Tab.saved)
60
61 #if os(tvOS)
62 NavigationStack {
63 SettingsView()
64 }
49 - .tabItem {
50 - Label("Settings", systemImage: "gearshape.2")
51 - }
65 + .tabItem { Label("Settings", systemImage: "gearshape.2") }
66 .tag(Tab.settings)
67 #endif
68 }
@@ -56,22 +70,16 @@ struct AppRootView: View {
70 .background(KleponColor.background.ignoresSafeArea())
71 #if !os(tvOS)
72 .sheet(isPresented: $showingSettings) {
59 - NavigationStack {
60 - SettingsView()
61 - }
62 - #if os(macOS) || os(visionOS)
63 - .frame(minWidth: 480, minHeight: 620)
64 - #endif
73 + NavigationStack { SettingsView() }
74 + #if os(macOS) || os(visionOS)
75 + .frame(minWidth: 480, minHeight: 620)
76 + #endif
77 }
78 #endif
79 .kleponOnboardingPresentation(isPresented: $showingOnboarding) {
80 OnboardingView(
69 - onBrowseFirst: {
70 - appState.completeOnboarding()
71 - },
72 - onComplete: {
73 - appState.completeOnboarding()
74 - }
81 + onBrowseFirst: { appState.completeOnboarding() },
82 + onComplete: { appState.completeOnboarding() }
83 )
84 #if os(macOS) || os(visionOS)
85 .frame(minWidth: 640, minHeight: 760)
@@ -88,4 +96,84 @@ struct AppRootView: View {
96 .frame(minWidth: 980, minHeight: 720)
97 #endif
98 }
99 +
100 + // MARK: – iPad root (NavigationSplitView)
101 +
102 + #if os(iOS)
103 + private var iPadRootView: some View {
104 + NavigationSplitView(columnVisibility: .constant(.all)) {
105 + iPadSidebar
106 + .navigationSplitViewColumnWidth(min: 240, ideal: 280, max: 320)
107 + } detail: {
108 + iPadDetail
109 + }
110 + .navigationSplitViewStyle(.balanced)
111 + .tint(KleponColor.accent)
112 + .sheet(isPresented: $showingSettings) {
113 + NavigationStack { SettingsView() }
114 + .presentationDetents([.large])
115 + }
116 + .kleponOnboardingPresentation(isPresented: $showingOnboarding) {
117 + OnboardingView(
118 + onBrowseFirst: { appState.completeOnboarding() },
119 + onComplete: { appState.completeOnboarding() }
120 + )
121 + }
122 + .task {
123 + showingOnboarding = !appState.hasCompletedOnboarding
124 + appState.guideEngine.refreshStorageUsage()
125 + }
126 + .onChange(of: appState.hasCompletedOnboarding) { _, newValue in
127 + showingOnboarding = !newValue
128 + }
129 + }
130 +
131 + private var iPadSidebar: some View {
132 + List(selection: $sidebarSelection) {
133 + Section {
134 + Label("Discover", systemImage: "sparkles")
135 + .tag(Tab.discover)
136 + Label("Search", systemImage: "magnifyingglass")
137 + .tag(Tab.search)
138 + Label("Saved", systemImage: "heart")
139 + .tag(Tab.saved)
140 + } header: {
141 + Text("Klepon")
142 + .font(KleponTypography.caption)
143 + .foregroundStyle(KleponColor.textSecondary)
144 + }
145 + }
146 + .listStyle(.sidebar)
147 + .background(KleponColor.background)
148 + .toolbar {
149 + ToolbarItem(placement: .topBarTrailing) {
150 + Button {
151 + showingSettings = true
152 + } label: {
153 + Image(systemName: "gearshape.2")
154 + }
155 + .tint(KleponColor.accent)
156 + }
157 + }
158 + .navigationTitle("Klepon")
159 + }
160 +
161 + @ViewBuilder
162 + private var iPadDetail: some View {
163 + switch sidebarSelection ?? .discover {
164 + case .discover:
165 + NavigationStack {
166 + DiscoverView(showingSettings: $showingSettings)
167 + }
168 + case .search:
169 + NavigationStack {
170 + SearchView()
171 + }
172 + case .saved:
173 + NavigationStack {
174 + SavedView()
175 + }
176 + }
177 + }
178 + #endif
179 }
Klepon/App/KleponApp.swift
+2 -1
@@ -13,7 +13,8 @@ struct KleponApp: App {
13 .environmentObject(appState.recentlyViewedStore)
14 .environmentObject(appState.guideEngine)
15 #if os(iOS)
16 - .preferredColorScheme(.light)
16 + .preferredColorScheme(
17 + UIDevice.current.userInterfaceIdiom == .pad ? nil : .light)
18 #endif
19 }
20 #if os(macOS) || os(visionOS)
Klepon/App/PlatformViewSupport.swift
+17
@@ -13,6 +13,15 @@ extension ToolbarItemPlacement {
13 }
14
15 extension View {
16 + @ViewBuilder
17 + func kleponReadableContentWidth() -> some View {
18 + #if os(iOS)
19 + self.frame(maxWidth: 720)
20 + #else
21 + self
22 + #endif
23 + }
24 +
25 @ViewBuilder
26 func kleponInteractiveButtonStyle() -> some View {
27 #if os(tvOS) || os(visionOS)
@@ -47,6 +56,14 @@ extension View {
56 ) -> some View {
57 #if os(macOS) || os(tvOS) || os(visionOS)
58 self.sheet(isPresented: isPresented, content: content)
59 + #elseif os(iOS)
60 + // iPad uses a sheet so the sidebar remains visible behind the cover;
61 + // iPhone uses fullScreenCover for the immersive first-launch feel.
62 + if UIDevice.current.userInterfaceIdiom == .pad {
63 + self.sheet(isPresented: isPresented, content: content)
64 + } else {
65 + self.fullScreenCover(isPresented: isPresented, content: content)
66 + }
67 #else
68 self.fullScreenCover(isPresented: isPresented, content: content)
69 #endif
Klepon/Assets.xcassets/AppIcon.appiconset/Contents.json
+112 -13
@@ -1,17 +1,116 @@
1 {
2 - "images" : [
3 - { "idiom" : "iphone", "size" : "20x20", "scale" : "2x", "filename" : "Icon-20@2x.png" },
4 - { "idiom" : "iphone", "size" : "20x20", "scale" : "3x", "filename" : "Icon-20@3x.png" },
5 - { "idiom" : "iphone", "size" : "29x29", "scale" : "2x", "filename" : "Icon-29@2x.png" },
6 - { "idiom" : "iphone", "size" : "29x29", "scale" : "3x", "filename" : "Icon-29@3x.png" },
7 - { "idiom" : "iphone", "size" : "40x40", "scale" : "2x", "filename" : "Icon-40@2x.png" },
8 - { "idiom" : "iphone", "size" : "40x40", "scale" : "3x", "filename" : "Icon-40@3x.png" },
9 - { "idiom" : "iphone", "size" : "60x60", "scale" : "2x", "filename" : "Icon-60@2x.png" },
10 - { "idiom" : "iphone", "size" : "60x60", "scale" : "3x", "filename" : "Icon-60@3x.png" },
11 - { "idiom" : "ios-marketing", "size" : "1024x1024", "scale" : "1x", "filename" : "Icon-1024.png" }
2 + "images": [
3 + {
4 + "idiom": "iphone",
5 + "size": "20x20",
6 + "scale": "2x",
7 + "filename": "Icon-20@2x.png"
8 + },
9 + {
10 + "idiom": "iphone",
11 + "size": "20x20",
12 + "scale": "3x",
13 + "filename": "Icon-20@3x.png"
14 + },
15 + {
16 + "idiom": "iphone",
17 + "size": "29x29",
18 + "scale": "2x",
19 + "filename": "Icon-29@2x.png"
20 + },
21 + {
22 + "idiom": "iphone",
23 + "size": "29x29",
24 + "scale": "3x",
25 + "filename": "Icon-29@3x.png"
26 + },
27 + {
28 + "idiom": "iphone",
29 + "size": "40x40",
30 + "scale": "2x",
31 + "filename": "Icon-40@2x.png"
32 + },
33 + {
34 + "idiom": "iphone",
35 + "size": "40x40",
36 + "scale": "3x",
37 + "filename": "Icon-40@3x.png"
38 + },
39 + {
40 + "idiom": "iphone",
41 + "size": "60x60",
42 + "scale": "2x",
43 + "filename": "Icon-60@2x.png"
44 + },
45 + {
46 + "idiom": "iphone",
47 + "size": "60x60",
48 + "scale": "3x",
49 + "filename": "Icon-60@3x.png"
50 + },
51 + {
52 + "idiom": "ipad",
53 + "size": "20x20",
54 + "scale": "1x",
55 + "filename": "Icon-ipad-20.png"
56 + },
57 + {
58 + "idiom": "ipad",
59 + "size": "20x20",
60 + "scale": "2x",
61 + "filename": "Icon-ipad-40.png"
62 + },
63 + {
64 + "idiom": "ipad",
65 + "size": "29x29",
66 + "scale": "1x",
67 + "filename": "Icon-ipad-29.png"
68 + },
69 + {
70 + "idiom": "ipad",
71 + "size": "29x29",
72 + "scale": "2x",
73 + "filename": "Icon-ipad-58.png"
74 + },
75 + {
76 + "idiom": "ipad",
77 + "size": "40x40",
78 + "scale": "1x",
79 + "filename": "Icon-ipad-40.png"
80 + },
81 + {
82 + "idiom": "ipad",
83 + "size": "40x40",
84 + "scale": "2x",
85 + "filename": "Icon-ipad-80.png"
86 + },
87 + {
88 + "idiom": "ipad",
89 + "size": "76x76",
90 + "scale": "1x",
91 + "filename": "Icon-ipad-76.png"
92 + },
93 + {
94 + "idiom": "ipad",
95 + "size": "76x76",
96 + "scale": "2x",
97 + "filename": "Icon-ipad-152.png"
98 + },
99 + {
100 + "idiom": "ipad",
101 + "size": "83.5x83.5",
102 + "scale": "2x",
103 + "filename": "Icon-ipad-167.png"
104 + },
105 + {
106 + "idiom": "ios-marketing",
107 + "size": "1024x1024",
108 + "scale": "1x",
109 + "filename": "Icon-1024.png"
110 + }
111 ],
13 - "info" : {
14 - "author" : "xcode",
15 - "version" : 1
112 + "info": {
113 + "author": "xcode",
114 + "version": 1
115 }
116 }
Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-152.png
Binary files /dev/null and b/Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-152.png differ
Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-167.png
Binary files /dev/null and b/Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-167.png differ
Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-20.png
Binary files /dev/null and b/Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-20.png differ
Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-29.png
Binary files /dev/null and b/Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-29.png differ
Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-40.png
Binary files /dev/null and b/Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-40.png differ
Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-58.png
Binary files /dev/null and b/Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-58.png differ
Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-76.png
Binary files /dev/null and b/Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-76.png differ
Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-80.png
Binary files /dev/null and b/Klepon/Assets.xcassets/AppIcon.appiconset/Icon-ipad-80.png differ
Klepon/Info.plist
+7 -2
@@ -43,7 +43,12 @@
43 <array>
44 <string>UIInterfaceOrientationPortrait</string>
45 </array>
46 - <key>UIUserInterfaceStyle</key>
47 - <string>Light</string>
46 + <key>UISupportedInterfaceOrientations~ipad</key>
47 + <array>
48 + <string>UIInterfaceOrientationPortrait</string>
49 + <string>UIInterfaceOrientationPortraitUpsideDown</string>
50 + <string>UIInterfaceOrientationLandscapeLeft</string>
51 + <string>UIInterfaceOrientationLandscapeRight</string>
52 + </array>
53 </dict>
54 </plist>
KleponMac/KleponMac.entitlements
+4 -2
@@ -2,13 +2,15 @@
2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 <plist version="1.0">
4 <dict>
5 - <key>com.apple.security.app-sandbox</key>
5 + <key>com.apple.developer.kernel.increased-memory-limit</key>
6 <true/>
7 - <key>com.apple.security.network.client</key>
7 + <key>com.apple.security.app-sandbox</key>
8 <true/>
9 <key>com.apple.security.application-groups</key>
10 <array>
11 <string>group.com.ondeinference.apps</string>
12 </array>
13 + <key>com.apple.security.network.client</key>
14 + <true/>
15 </dict>
16 </plist>
Scripts/OndePrivacyInfo.xcprivacy new
+72
@@ -0,0 +1,72 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 +<plist version="1.0">
4 +<dict>
5 + <!--
6 + Onde Inference — Required Reasons API declarations
7 + https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api
8 +
9 + These APIs are called by the Rust static library (mistral.rs / tokio / reqwest)
10 + embedded in OndeFramework.xcframework. None are used for tracking.
11 +
12 + Verified with:
13 + nm -u libonde-macos.a | grep -E "_sysctl|_stat|_mach_absolute|_gettimeofday"
14 + -->
15 + <key>NSPrivacyTracking</key>
16 + <false/>
17 + <key>NSPrivacyTrackingDomains</key>
18 + <array/>
19 + <key>NSPrivacyCollectedDataTypes</key>
20 + <array/>
21 + <key>NSPrivacyAccessedAPITypes</key>
22 + <array>
23 + <!--
24 + sysctl / sysctlbyname / sysctlnametomib
25 + Used by: tokio (thread / CPU count), mistral.rs (hardware capability detection),
26 + sysinfo (memory pressure monitoring for model loading decisions).
27 +
28 + SystemBootTime — 35F9.1: Declare timestamp for measuring elapsed inference time.
29 + -->
30 + <dict>
31 + <key>NSPrivacyAccessedAPIType</key>
32 + <string>NSPrivacyAccessedAPICategorySystemBootTime</string>
33 + <key>NSPrivacyAccessedAPITypeReasons</key>
34 + <array>
35 + <string>35F9.1</string>
36 + </array>
37 + </dict>
38 + <!--
39 + sysctl (hw.memsize, hw.physicalcpu, hw.logicalcpu, hw.cpufrequency)
40 + Used by: mistral.rs / candle to size KV cache and choose quantisation level
41 + based on available RAM and CPU topology.
42 +
43 + DiskSpace — E174.1: Check available disk space before downloading model weights.
44 + -->
45 + <dict>
46 + <key>NSPrivacyAccessedAPIType</key>
47 + <string>NSPrivacyAccessedAPICategoryDiskSpace</string>
48 + <key>NSPrivacyAccessedAPITypeReasons</key>
49 + <array>
50 + <string>E174.1</string>
51 + </array>
52 + </dict>
53 + <!--
54 + stat / fstat / lstat / fstatat / fstatfs / fstatvfs
55 + Used by: hf-hub (checking whether a cached model file exists and is complete),
56 + tokio::fs (async file I/O underlying model weight streaming).
57 +
58 + FileTimestamp — C617.1: Read file modification timestamps to validate cached
59 + model files against the HuggingFace Hub manifest and avoid re-downloading
60 + unchanged weights.
61 + -->
62 + <dict>
63 + <key>NSPrivacyAccessedAPIType</key>
64 + <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
65 + <key>NSPrivacyAccessedAPITypeReasons</key>
66 + <array>
67 + <string>C617.1</string>
68 + </array>
69 + </dict>
70 + </array>
71 +</dict>
72 +</plist>
project.yml
+56 -2
@@ -109,7 +109,11 @@ targets:
109 UISceneClassName: UIWindowScene
110 UISupportedInterfaceOrientations:
111 - UIInterfaceOrientationPortrait
112 - UIUserInterfaceStyle: Light
112 + UISupportedInterfaceOrientations~ipad:
113 + - UIInterfaceOrientationPortrait
114 + - UIInterfaceOrientationPortraitUpsideDown
115 + - UIInterfaceOrientationLandscapeLeft
116 + - UIInterfaceOrientationLandscapeRight
117 entitlements:
118 path: Klepon/Klepon.entitlements
119 properties:
@@ -123,6 +127,31 @@ targets:
127 - package: Onde
128 product: Onde
129 - target: Klepon Watch
130 + preBuildScripts:
131 + - name: Inject PrivacyInfo into OndeFramework
132 + shell: /bin/sh
133 + basedOnDependencyAnalysis: false
134 + showEnvVars: false
135 + script: |
136 + set -eu
137 + # PrivacyInfo.xcprivacy is required inside every XCFramework slice.
138 + # Apple's scanner does not accept the app-level manifest as a substitute
139 + # for manifests inside nested third-party framework bundles.
140 + # BUILD_DIR is .../DerivedData/<name>/Build/Intermediates.noindex
141 + # SourcePackages is always two levels up from BUILD_DIR.
142 + XCFW="${BUILD_DIR}/../../SourcePackages/artifacts/onde-swift/OndeFramework/OndeFramework.xcframework"
143 + PRIVACY_SRC="${SRCROOT}/Scripts/OndePrivacyInfo.xcprivacy"
144 + if [ ! -d "${XCFW}" ]; then
145 + echo "warning: OndeFramework.xcframework not found at ${XCFW} — skipping PrivacyInfo injection"
146 + exit 0
147 + fi
148 + for SLICE in "${XCFW}"/*/; do
149 + DEST="${SLICE}PrivacyInfo.xcprivacy"
150 + if [ ! -f "${DEST}" ]; then
151 + cp "${PRIVACY_SRC}" "${DEST}"
152 + echo "Injected PrivacyInfo.xcprivacy into ${SLICE}"
153 + fi
154 + done
155 postBuildScripts:
156 - name: Update CFBundleVersion
157 shell: /bin/sh
@@ -159,7 +188,7 @@ targets:
188 ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
189 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
190 ENABLE_USER_SCRIPT_SANDBOXING: NO
162 - TARGETED_DEVICE_FAMILY: 1
191 + TARGETED_DEVICE_FAMILY: "1,2"
192 "EXCLUDED_ARCHS[sdk=iphonesimulator*]": x86_64
193 configs:
194 Debug:
@@ -213,13 +242,38 @@ targets:
242 com.apple.security.network.client: true
243 com.apple.security.application-groups:
244 - group.com.ondeinference.apps
245 + com.apple.developer.kernel.increased-memory-limit: true
246 configFiles:
247 Debug: Configs/Signing.xcconfig
248 Release: Configs/Signing.xcconfig
249 dependencies:
250 - package: Onde
251 product: Onde
252 + # SystemConfiguration is a genuine transitive dependency of the Onde Rust
253 + # static library (reqwest → native-tls → SCDynamicStore / SCNetworkInterface).
254 + # It is required by the macOS linker to resolve symbols in libonde-macos.a
255 + # and is NOT present in the iOS/tvOS/watchOS slices.
256 - sdk: SystemConfiguration.framework
257 + preBuildScripts:
258 + - name: Inject PrivacyInfo into OndeFramework
259 + shell: /bin/sh
260 + basedOnDependencyAnalysis: false
261 + showEnvVars: false
262 + script: |
263 + set -eu
264 + XCFW="${BUILD_DIR}/../../SourcePackages/artifacts/onde-swift/OndeFramework/OndeFramework.xcframework"
265 + PRIVACY_SRC="${SRCROOT}/Scripts/OndePrivacyInfo.xcprivacy"
266 + if [ ! -d "${XCFW}" ]; then
267 + echo "warning: OndeFramework.xcframework not found at ${XCFW} — skipping PrivacyInfo injection"
268 + exit 0
269 + fi
270 + for SLICE in "${XCFW}"/*/; do
271 + DEST="${SLICE}PrivacyInfo.xcprivacy"
272 + if [ ! -f "${DEST}" ]; then
273 + cp "${PRIVACY_SRC}" "${DEST}"
274 + echo "Injected PrivacyInfo.xcprivacy into ${SLICE}"
275 + fi
276 + done
277 postBuildScripts:
278 - name: Update CFBundleVersion
279 shell: /bin/sh