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