Add support for macOS and backport to older OS versions

This commit is contained in:
2026-09-03 20:06:59 +03:00
parent abf7cf2e83
commit deef01a58a
13 changed files with 123 additions and 39 deletions
+5 -1
View File
@@ -7,4 +7,8 @@ org.gradle.configuration-cache=true
org.gradle.caching=true org.gradle.caching=true
#Android #Android
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.useAndroidX=true android.useAndroidX=true
#Compose Multiplatform
org.jetbrains.compose.experimental.macos.enabled=true
#Kotlin/Native
kotlin.native.ignoreDisabledTargets=true
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

+47 -26
View File
@@ -11,7 +11,7 @@
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
D262C3356B38E7E13FFE64E4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { D262C3356B38E7E13FFE64E4 /* Exceptions for "iosApp" folder in "iosApp" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
Info.plist, Info.plist,
@@ -23,21 +23,13 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
24D66E9CCFCEB2D844328B79 /* Configuration */ = { 24D66E9CCFCEB2D844328B79 /* Configuration */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
explicitFileTypes = {
};
explicitFolders = (
);
path = Configuration; path = Configuration;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
55D8B06C58654F251C5B3381 /* iosApp */ = { 55D8B06C58654F251C5B3381 /* iosApp */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = ( exceptions = (
D262C3356B38E7E13FFE64E4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, D262C3356B38E7E13FFE64E4 /* Exceptions for "iosApp" folder in "iosApp" target */,
);
explicitFileTypes = {
};
explicitFolders = (
); );
path = iosApp; path = iosApp;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -84,6 +76,7 @@
5BCD356C334571BCAA0967CA /* Sources */, 5BCD356C334571BCAA0967CA /* Sources */,
C2B5A146484C5EC128AEFD65 /* Frameworks */, C2B5A146484C5EC128AEFD65 /* Frameworks */,
96EB3DBC5DECE4CD4D246992 /* Resources */, 96EB3DBC5DECE4CD4D246992 /* Resources */,
1DADA2DD199949A498015FFD /* Sanitize Moko Resource Bundle Names */,
); );
buildRules = ( buildRules = (
); );
@@ -181,6 +174,24 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"$SRCROOT/../gradlew\" -p \"$SRCROOT/../\" :shared:copyFrameworkResourcesToApp \\\n -Pmoko.resources.PLATFORM_NAME=\"$PLATFORM_NAME\" \\\n -Pmoko.resources.CONFIGURATION=\"$CONFIGURATION\" \\\n -Pmoko.resources.ARCHS=\"$ARCHS\" \\\n -Pmoko.resources.BUILT_PRODUCTS_DIR=\"$BUILT_PRODUCTS_DIR\" \\\n -Pmoko.resources.CONTENTS_FOLDER_PATH=\"$CONTENTS_FOLDER_PATH\" \n"; shellScript = "\"$SRCROOT/../gradlew\" -p \"$SRCROOT/../\" :shared:copyFrameworkResourcesToApp \\\n -Pmoko.resources.PLATFORM_NAME=\"$PLATFORM_NAME\" \\\n -Pmoko.resources.CONFIGURATION=\"$CONFIGURATION\" \\\n -Pmoko.resources.ARCHS=\"$ARCHS\" \\\n -Pmoko.resources.BUILT_PRODUCTS_DIR=\"$BUILT_PRODUCTS_DIR\" \\\n -Pmoko.resources.CONTENTS_FOLDER_PATH=\"$CONTENTS_FOLDER_PATH\" \n";
}; };
1DADA2DD199949A498015FFD /* Sanitize Moko Resource Bundle Names */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Sanitize Moko Resource Bundle Names";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"$PLATFORM_NAME\" != \"macosx\" ]; then\n exit 0\nfi\n# moko-resources names its resource bundle after the Kotlin klib unique_name (group:module),\n# e.g. SatsPrice:shared.bundle, and drops it directly under Contents/. Colons break\n# codesign, and a loose bundle outside Contents/Resources isn't recognized as a proper\n# nested bundle either, so the final app-level CodeSign phase fails with \"code object is\n# not signed at all\" unless we rename and relocate it. NSBundle resolves this bundle by\n# directory identity, not by name/path, so moko-resources/localization lookups still work.\nfind \"$CODESIGNING_FOLDER_PATH\" -type d -name \"*:*.bundle\" | while IFS= read -r bundle; do\n safe_name=$(basename \"$bundle\" | tr ':' '_')\n mkdir -p \"$CODESIGNING_FOLDER_PATH/Contents/Resources\"\n dest=\"$CODESIGNING_FOLDER_PATH/Contents/Resources/$safe_name\"\n rm -rf \"$dest\"\n mv \"$bundle\" \"$dest\"\ndone\n";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -200,18 +211,21 @@
ARCHS = arm64; ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = iosApp/macOS.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "${TEAM_ID}"; DEVELOPMENT_TEAM = S99A5B637C;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphone*]" = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphone*]" = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*]" = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; "INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad[sdk=iphone*]" = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
"INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone[sdk=iphone*]" = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -270,11 +284,13 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = iphoneos; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
@@ -334,12 +350,14 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
}; };
@@ -351,18 +369,21 @@
ARCHS = arm64; ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = iosApp/macOS.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "${TEAM_ID}"; DEVELOPMENT_TEAM = S99A5B637C;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphone*]" = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphone*]" = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*]" = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; "INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad[sdk=iphone*]" = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
"INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone[sdk=iphone*]" = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
+35 -7
View File
@@ -6,7 +6,7 @@ struct ContentView: View {
@State private var showCurrencyPicker = false @State private var showCurrencyPicker = false
var body: some View { var body: some View {
NavigationView { NavigationStack {
Group { Group {
if let state = viewModel.state { if let state = viewModel.state {
form(for: state) form(for: state)
@@ -121,13 +121,18 @@ struct ContentView: View {
onChange: { viewModel.onFiatAmountChanged(code: row.code, value: $0) } onChange: { viewModel.onFiatAmountChanged(code: row.code, value: $0) }
) )
} }
#if os(iOS)
.onDelete { indexSet in .onDelete { indexSet in
for index in indexSet { for index in indexSet {
viewModel.onFiatCurrencyToggled(state.fiatRows[index].code) viewModel.onFiatCurrencyToggled(state.fiatRows[index].code)
} }
} }
#endif
} }
} }
#if os(macOS)
.formStyle(.grouped)
#endif
.sheet(isPresented: $showCurrencyPicker) { .sheet(isPresented: $showCurrencyPicker) {
CurrencyPickerSheet( CurrencyPickerSheet(
currencies: state.availableCurrencies, currencies: state.availableCurrencies,
@@ -142,7 +147,7 @@ struct ContentView: View {
private func amountRow( private func amountRow(
label: String, label: String,
value: String, value: String,
keyboardType: UIKeyboardType, keyboardType: NumericFieldKeyboard,
sanitize: @escaping (String) -> String, sanitize: @escaping (String) -> String,
onChange: @escaping (String) -> Void onChange: @escaping (String) -> Void
) -> some View { ) -> some View {
@@ -150,7 +155,7 @@ struct ContentView: View {
Text(label) Text(label)
Spacer() Spacer()
NumericField( NumericField(
placeholder: label, placeholder: "",
value: value, value: value,
keyboardType: keyboardType, keyboardType: keyboardType,
sanitize: sanitize, sanitize: sanitize,
@@ -170,7 +175,7 @@ struct ContentView: View {
private struct NumericField: View { private struct NumericField: View {
let placeholder: String let placeholder: String
let value: String let value: String
let keyboardType: UIKeyboardType let keyboardType: NumericFieldKeyboard
let sanitize: (String) -> String let sanitize: (String) -> String
let onChange: (String) -> Void let onChange: (String) -> Void
var alignment: TextAlignment = .leading var alignment: TextAlignment = .leading
@@ -179,10 +184,15 @@ private struct NumericField: View {
var body: some View { var body: some View {
TextField(placeholder, text: $text) TextField(placeholder, text: $text)
.keyboardType(keyboardType) #if os(iOS)
.keyboardType(keyboardType.uiKeyboardType)
#endif
.multilineTextAlignment(alignment) .multilineTextAlignment(alignment)
#if os(macOS)
.frame(maxWidth: 140)
#endif
.onAppear { text = value } .onAppear { text = value }
.onChange(of: text) { _, newValue in .onChange(of: text) { newValue in
let sanitized = sanitize(newValue) let sanitized = sanitize(newValue)
if sanitized != newValue { if sanitized != newValue {
text = sanitized text = sanitized
@@ -191,7 +201,7 @@ private struct NumericField: View {
onChange(sanitized) onChange(sanitized)
} }
} }
.onChange(of: value) { _, newValue in .onChange(of: value) { newValue in
if newValue != text { if newValue != text {
text = newValue text = newValue
} }
@@ -216,3 +226,21 @@ private func sanitizeDecimalInput(_ raw: String) -> String {
private func sanitizeIntegerInput(_ raw: String) -> String { private func sanitizeIntegerInput(_ raw: String) -> String {
raw.filter { $0.isNumber } raw.filter { $0.isNumber }
} }
/// `UIKeyboardType` doesn't exist on macOS (no on-screen keyboard), so this stands in for it
/// across both platforms; only iOS actually applies it, via `uiKeyboardType` below.
private enum NumericFieldKeyboard {
case decimalPad
case numberPad
}
#if os(iOS)
private extension NumericFieldKeyboard {
var uiKeyboardType: UIKeyboardType {
switch self {
case .decimalPad: return .decimalPad
case .numberPad: return .numberPad
}
}
}
#endif
+9 -1
View File
@@ -10,7 +10,7 @@ struct CurrencyPickerSheet: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var body: some View { var body: some View {
NavigationView { NavigationStack {
List(currencies, id: \.code) { info in List(currencies, id: \.code) { info in
Button { Button {
onToggle(info.code) onToggle(info.code)
@@ -27,13 +27,21 @@ struct CurrencyPickerSheet: View {
} }
} }
.navigationTitle(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title)) .navigationTitle(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title))
#if os(iOS)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#endif
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button(IosLocalizationKt.localizedString(resource: MR.strings.shared.done)) { dismiss() } Button(IosLocalizationKt.localizedString(resource: MR.strings.shared.done)) { dismiss() }
} }
} }
} }
#if os(macOS)
// macOS sizes a .sheet() to its content's ideal size rather than the parent window's
// size (unlike iOS, which presents modally full-size); without an explicit frame here,
// the List has no size to lay out rows in and renders empty.
.frame(minWidth: 420, minHeight: 480)
#endif
} }
private func currencyLabel(for info: CurrencyInfo) -> String { private func currencyLabel(for info: CurrencyInfo) -> String {
+3
View File
@@ -5,6 +5,9 @@ struct iOSApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ContentView()
#if os(macOS)
.frame(minWidth: 420, minHeight: 500)
#endif
} }
} }
} }
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
+5 -4
View File
@@ -13,9 +13,10 @@ plugins {
kotlin { kotlin {
listOf( listOf(
iosArm64(), iosArm64(),
iosSimulatorArm64() iosSimulatorArm64(),
).forEach { iosTarget -> macosArm64()
iosTarget.binaries.framework { ).forEach { appleTarget ->
appleTarget.binaries.framework {
baseName = "Shared" baseName = "Shared"
isStatic = true isStatic = true
export(libs.moko.resources) export(libs.moko.resources)
@@ -87,7 +88,7 @@ kotlin {
implementation(libs.kotlinx.coroutinesTest) implementation(libs.kotlinx.coroutinesTest)
implementation(libs.ktor.client.mock) implementation(libs.ktor.client.mock)
} }
iosMain.dependencies { appleMain.dependencies {
implementation(libs.ktor.client.darwin) implementation(libs.ktor.client.darwin)
} }
jvmMain.dependencies { jvmMain.dependencies {
@@ -0,0 +1,9 @@
package xyz.tyiu.satsprice
import platform.Foundation.NSProcessInfo
class MacOSPlatform : Platform {
override val name: String = "macOS " + NSProcessInfo.processInfo.operatingSystemVersionString
}
actual fun getPlatform(): Platform = MacOSPlatform()