From deef01a58aa2753a365ee9337cefe01694578e13 Mon Sep 17 00:00:00 2001 From: Terry Yiu Date: Thu, 3 Sep 2026 20:06:59 +0300 Subject: [PATCH] Add support for macOS and backport to older OS versions --- gradle.properties | 6 +- iosApp/execution | Bin 0 -> 6538 bytes iosApp/iosApp.xcodeproj/project.pbxproj | 73 +++++++++++------- iosApp/iosApp/ContentView.swift | 42 ++++++++-- iosApp/iosApp/CurrencyPickerSheet.swift | 10 ++- iosApp/iosApp/iOSApp.swift | 3 + iosApp/iosApp/macOS.entitlements | 10 +++ shared/build.gradle.kts | 9 ++- .../xyz/tyiu/satsprice/IosPriceViewModel.kt | 0 .../tyiu/satsprice/SystemCurrencies.apple.kt} | 0 .../tyiu/satsprice/ui/Dimensions.apple.kt} | 0 .../xyz/tyiu/satsprice/ui/IosLocalization.kt | 0 .../xyz/tyiu/satsprice/Platform.macos.kt | 9 +++ 13 files changed, 123 insertions(+), 39 deletions(-) create mode 100644 iosApp/execution create mode 100644 iosApp/iosApp/macOS.entitlements rename shared/src/{iosMain => appleMain}/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt (100%) rename shared/src/{iosMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.ios.kt => appleMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.apple.kt} (100%) rename shared/src/{iosMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.ios.kt => appleMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.apple.kt} (100%) rename shared/src/{iosMain => appleMain}/kotlin/xyz/tyiu/satsprice/ui/IosLocalization.kt (100%) create mode 100644 shared/src/macosMain/kotlin/xyz/tyiu/satsprice/Platform.macos.kt diff --git a/gradle.properties b/gradle.properties index ee114b2..ca41477 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,8 @@ org.gradle.configuration-cache=true org.gradle.caching=true #Android android.nonTransitiveRClass=true -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +#Compose Multiplatform +org.jetbrains.compose.experimental.macos.enabled=true +#Kotlin/Native +kotlin.native.ignoreDisabledTargets=true \ No newline at end of file diff --git a/iosApp/execution b/iosApp/execution new file mode 100644 index 0000000000000000000000000000000000000000..5ef9bb018ea267eedd2dbff97d33ad047d4b1d0d GIT binary patch literal 6538 zcmZWs1yo!~vmO{+0t6?)Ex`tt!5MDU}2y{#g>Ue743;( z`W9eAh*P%15Ef9%ND+zYX2%rK3L;QTa&yu@82p4SlIpwZeeUbB-Ts_;yPfH>y~Ga$ zv_^uGxAHRp8`5WHY@JVKpEvLbc8Mu~xVh+Q9ciFASw3auRY1z>)$!&f*T**tLoZ~? zp;yl{`LuWghyV&@>|*rI0D&+lJiq~8%Ip?^T^PrclMGT{tH<8&&ke+$dE-=s*2v)0 zf@b)-E;VN(1I_^rA&3-Uot43h(oN=dS^6i6zey1}K(1CaSi+#}j-+Vt=XO+R zQOU`AN>$!QL$@^D0faPAqdQGEfU^jU--x^+W$wnytj}$@dv{Q@v5_=Ooap3(>F7 z+VQw!%DX1>+Ehz%LR(sC_^Bvt_-h~Gox~8jNw6jRU5vXzY*o-}n{c|g+22L66QGBg zQAt;|%aAjU#Ub`!%pH#=V)ez44!u053OOl!C~Lx2uc(rQf69GkB`8y*S?$K!58IGl z?W#t32j^ch>r^B4lDu@yNzked8fDF7Pd^e(Qq%YNuDMfY31f}dQ}p0uHo&KeSN+Z- zoKu)VVUykpbxcU(t9)SMly*x692sgI-1^49-esx9c6(Dgx0@Vs@H%6?~ z>x1|X8>Tl1?GPIHvv{Ycz*Z_Hz^!6|7A-y5s+uNp9NT2E+5lWEJ*lSMP{@&M&D^q_DR}LX%CSmnH{RdOcs`e=rd>boq#)0$gT7Gb3 zWG6a%z-vcBUM%G6V2*EMDf}Ca?2Ki~^U%nZQhH8PNvHhWBsM(Zh>@Lmk`E*m)C_}q z&@A%g%3bQ)t(c3b?3{b`tKcfkS9amPp`;xOCVkblT8z^OTC>V!#5R2|7*G2nyg8fJ zLM$SQFX)l?q6|0;v&+~Mor}T6WW}*1Z9d}%KAS`RS(I7SxEAJ{wY(>>l*Xqt@O@59 z7W5W8fTU;gsqI7cixnuMQ3G@}YQg@Tes4U; zlrrfgO1)TLfo)P?NS@P<8Yga6I6;2Tj&mJ+xx{sD^p4YofEWIuSWdwsQO6`Jop}V` zd)uhUcgAnEkm$My-WO`|X+%$%FrV-l_O?_R9FQt>6Z4|Ge`MrEi0wnXk@JCT!>H`G zIgxmv@gr`JyMihHqOtVpXK|IFB)%m4B+?{;K|~pzO`v+DwftpKuX63z z$glX5UVCr%NcR}IxVSjDu<9+js^_Wd(dvDzEhdjjTUBQ?8dbMTic1(LQ%jFkgp2%C zAMv9lYV&W$RV;7oHtTfeQRfBcbumLi3Xl{bU#gV~WD1nm6ME6X!S>SIKa0oFcQ-G9 z7w8uT@)1SJbef5k%_6lj=W$zZBYw;w(+K5Kl`YVcY3-l5*0>G2UhTQ=ToDXn^_-%8@S3RwcfT)HPbY6?+&n#6^kSKi-q0mAd{hit%KF?C+=sdx8xVluFH+l zZ8G!@_yC{`5DZ@rTn91&j}g-m&5))LFA={XjbREAh@qgOYvVg-S#;x`SbO^B9p#-v z6hpp-_JpX12%envGTz7$;(lR z8V*YHWGglyuAVS)?`w-WkJZI-=U<{Tz*$ZaQIwMZBJYt%6xRfFKS%>a_f>f}1?EbSw9i*WSTbi(+EAp{x%cU|%rw&Yx3E$9VpzF7`pn zkE>lcE47WSRj~uVj8K5^4s{02TYM>hDxaQsiMW&_+uX~dV&GulY=mKK58tIHc`bF0 za!$8qc|&~bV34?9e%P*`db@CwG{I8VnTFniXe>>zK-C(=9RFeDBD2}vJcmiHK{cko zc<|G?+loZHzEQ!Agj0J*ac;@VVOfwivboJj%CLQo36I5YCVjAIu<&rtHriIh2uD_< z$I|0%=IsIj1^O?-I%ZBot?yOd$|w05J(E3rkx*u51|9v2nS^dy<{pWj$Ov*j+y{C6 zaEb7;jk`vJW&7I~&k7IveZRtGd6*vc7?=b#x70KYuAI`}&ov*@`xu@+cAQ0Rk}@u8 z*cwRdz18-#OFk~in9?al*A&*{@RVQInV$bu4=w-Hc4XTudz*4Oc^Pr3kH3mP@`{PO zZ1JR;z4XjF8LHkT(G@Mr(>6!5=xSBFymiMcp*z^{eVJj&%S892q1U=}@9ePq5OIP0 zSZdbk4*3eJo*(l=%HczuY)xq?UD1|lxwqY+eb;Zsy`SKB`eWIXf`r#d2X+Z>cR;Uxa41ZT<41a`jL*tV&)nK@}g<70cD}CzD?$t$iG^!nU85KHQ7{3TLD*Q2daI#_zV3 znro<#xs@s8)q5|uuRT^9I$Ax(mF3RAJbN_nccM9Ci1ne#jnlxf{io0Py0Pr`=TYiZ z9}sE#YscGE#_x>Vl@fY~-wB|Hy_b{w`aD5A*-OK%w`UrizMi%KO5D&5oQQgWdM|%hVx5yGP~6YlH39r<0T{w(4vE0Ofhh}tu4m$wD!~U~^ zauyacdOmpK1=UGT)gA_)*2-uh+0kx+_v+CW7;ygq@E-AU_bWhr91wO6*y#?ZcDrHX z@i<0tEL>i)mfeC=K z2d*E())8h}(&qB=00x+h0ziNx0U*L8I2Z}Rz4}L%fO`V~{-uWp079(*2!H!1!1$jL z2ctij|8QVJ2ml$jdI=+sT=;*ywQ_;~$h>)_?mj@Uo|51aeDE@MBwGpJ!l2@XDI5?Y8aI>(ouu=)3QBY6_IGdV-Rm3I! zhQn5ZRFCjX^{by)~a0QAp}38C?gO7X!q@wJt>q8g0DUa`N% zYuNk-Mt?YrkhsT0+B;wzURqpK%>%Am%TUTxooLzHVi;Ps`^A*x?c@)!s;U$jF)XA8 z)JUvgY|)pULU=?{tr}Nrk;kuUs^ep&vHZ0bB4-2Fa?13pP*fdzgomP1)=N0NEQ*%h zKOG;WuQxAwOKy9uc(k2AKKi!vU5^q+%M$WOeH^raFLBA2Q^Is!&E0p>cf*W2$t8JF z5+n0@p>0JRdE5h=YcWh<#6T$lea=Uy`8<3)J{8OAwtL5VduOyXFZ1)P$H0D%yoDO@W|%`SBL58>HLR>hXc06^z2y|Z9n~2)EWLO)Bd0^0F1+%WO_-PQ0a> zW;7f(L__Y@(i4|u`~_($Kcha#zY3e~hXhexsFf#?zHE zk(LKOsVB{#--tX}%WcfhK%<0|k|`*>3P)lG!MwjEJGU(@4`>qDW0!|!_y(%|b#nUR z!j*7E-RLEsLD+lpWVvF&dvEQxlK4hcwn6b#Vtq9GUIBbJdB7hOBcIJ$6J?5duCK14 z08VhTXCl9VfL=pwtqXI*w*_ZM#}f@Ef^Qik^teaCdRKW(C`Qy#LHOSOq%{>hUjj+N z*zhz8eLSvWDXFQ#2u$sjh*V;6*Ec68`d*oZ%?)*R7nujZ^nvph0$lDNQd&5mD#-LD zlA9w0Sp>;)je+~!N?%`JB`{^4^S;Du3Edgzl|%&_*y>C`ibja`6DOaBeltNiA2+N! zsEoEXB#sO#!1`bCiswe3Pwo z8$N0XO4$!zBa;6Ixv19WXM$YVM9iLZ`YBsu110guL2Ha{hpm;hl__`+L$>hZw8@pb3u zsG=_x;r>y9>%?pZ*;fbw7l|jc#Q;ZRHjmLOfHnO6RM7%Q=h{U@d3kHtOc&C&b`!Ln z&~AKeerWvg&}iDi0U*wrINWSGH8nWyM0T_H;MV%1z6!#e0VFE40_9+2=}!*?p#)$h z4vaNpR8>_`NAB+KGQ`<$`4?iD_PKSYamx6U`8@cL*Rp)3 z{CulrJN-ggB&LV~Z3^K|k5(Pd^Oq7av~wNzS0goNXZ(0$ffZ&<6Bz@h;;(JP4e96& zcXxJfc=oC92Wv6|OkwqFWhsm>UcrC<*@WIgq`r-mS(+IZ*x3>OK=7Oi&C0SAjDBCE z+XPBYN?N2PhR}(oJU0s6?uV!nmQDx`e37yajL`lz)$;|6&rACYE^*zK*!_1%Z&A_P zOeHVVLw|BS;aaB3R7Qp+Z11ic4%>{nZSePmNG6vs{jp8yiJo28%=7L$a@c|J{9LD4 z3cD&G(ET|L4mCt=a7FR#)OS$S9Py%w1$JRu#nI@r1VYy@%YM*B#^RLMe@=)Gk zV`ZzMd7bQtzB-}6x9)_1Qc?n5CU3o$`@}iGeR`iln*M!Qf!oTx)pUi8|Jc`I>AEdz z?sIABB@QH_PXA<=0K`8UQColMVsBr8Ro!F(4jCI9YyPCckbK0hIJaJ`IMxXrHZ@F5 zb%5RI>+jYrxV;Sy1GSeIwA}st+H!6;_Eu>Xt_Iw8y42KOMiR;vD$=w!G<+b5x-h?G z>TyFPBPIQfnJ2_JKF!0u1n-UaVfH=H#KgpcA?93vbtrj|dS#I?SiAD;`TaIM&sIWd z=!}yO_a&P94TK=2d#%~$sZh2}>-!7vb4HDT`?G;z`*7kH1YI4S7DOK`+s4?-$I;D2 z@UZBSGdN2Kh3TEWPA!W4FmQ&D%;1Xh9)T%?5?$8#n}&v_LxO<9tc{ZOJQM5hrV_=Z zrv7_wHa4f1e!@<>%e7Tij_)>4HaF0N@vZ~7%BAgxX*E|~d3{v8Le=Di5Nx%?tZZy- zp!PYX%(~_&HGOO*by!3_FQoSz1+!Y$!6K0bau*!Z;7Tcu=*haf(=F6X4L&V&5aSyC%c9TKjEwIgbA_j#|)F{cBnD%E_Q zPrEm-`6nl2{Tn#sZhuL3-iL;UUXKnB>&eq4^0XLN0~g-Tf3Po488fdPQ~5f14m%#@ z4-x~_d;GYnw|;GKbdcz)L`ekwUQY!*;-%+*h>eZC{@v_;y!M{m^KHCTCcNVtg2=<# zR4b59=@gpMmS&-fw!x731Qxl#4XdEwBl0)CDgmajCZ?xl1aqgibiS{Djd4NCuStv@ zr!doX3G0|LP&;`Q?Q!U;oy!MrZq*mTjSFFmlMdvGIcS~L41qyYm z(8>@vP|eTath;}admN~lH2BU(Bid5|ekzI-HM~MsYB(i9mN|p7)0EO}QeP{VbK2B8O_YS~)|<9Lm0AlM?7 z&Y;}AR;$Ww%5~G%n;i*mh02cTbmWB%c`2h}tCL2C^eS2YD6v-NA#)R)D$axuYT!TQ zU`p!qvnwYzWi=OwNczXXlOlrAT=90^{A`1V!W}65sP|`335I<-0MhRi#4E(!2mBAZ CND*)V literal 0 HcmV?d00001 diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index af35cb2..a1133c8 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - D262C3356B38E7E13FFE64E4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + D262C3356B38E7E13FFE64E4 /* Exceptions for "iosApp" folder in "iosApp" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, @@ -23,21 +23,13 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 24D66E9CCFCEB2D844328B79 /* Configuration */ = { isa = PBXFileSystemSynchronizedRootGroup; - explicitFileTypes = { - }; - explicitFolders = ( - ); path = Configuration; sourceTree = ""; }; 55D8B06C58654F251C5B3381 /* iosApp */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( - D262C3356B38E7E13FFE64E4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, - ); - explicitFileTypes = { - }; - explicitFolders = ( + D262C3356B38E7E13FFE64E4 /* Exceptions for "iosApp" folder in "iosApp" target */, ); path = iosApp; sourceTree = ""; @@ -84,6 +76,7 @@ 5BCD356C334571BCAA0967CA /* Sources */, C2B5A146484C5EC128AEFD65 /* Frameworks */, 96EB3DBC5DECE4CD4D246992 /* Resources */, + 1DADA2DD199949A498015FFD /* Sanitize Moko Resource Bundle Names */, ); buildRules = ( ); @@ -181,6 +174,24 @@ 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"; }; + 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 */ /* Begin PBXSourcesBuildPhase section */ @@ -200,18 +211,21 @@ ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = iosApp/macOS.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; + DEVELOPMENT_TEAM = S99A5B637C; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = iosApp/Info.plist; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphone*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphone*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*]" = YES; + "INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad[sdk=iphone*]" = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + "INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone[sdk=iphone*]" = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -270,11 +284,13 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - SDKROOT = iphoneos; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; }; @@ -334,12 +350,14 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -351,18 +369,21 @@ ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = iosApp/macOS.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; + DEVELOPMENT_TEAM = S99A5B637C; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = iosApp/Info.plist; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphone*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphone*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*]" = YES; + "INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad[sdk=iphone*]" = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + "INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone[sdk=iphone*]" = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 27c8f2e..43169bd 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -6,7 +6,7 @@ struct ContentView: View { @State private var showCurrencyPicker = false var body: some View { - NavigationView { + NavigationStack { Group { if let state = viewModel.state { form(for: state) @@ -121,13 +121,18 @@ struct ContentView: View { onChange: { viewModel.onFiatAmountChanged(code: row.code, value: $0) } ) } + #if os(iOS) .onDelete { indexSet in for index in indexSet { viewModel.onFiatCurrencyToggled(state.fiatRows[index].code) } } + #endif } } + #if os(macOS) + .formStyle(.grouped) + #endif .sheet(isPresented: $showCurrencyPicker) { CurrencyPickerSheet( currencies: state.availableCurrencies, @@ -142,7 +147,7 @@ struct ContentView: View { private func amountRow( label: String, value: String, - keyboardType: UIKeyboardType, + keyboardType: NumericFieldKeyboard, sanitize: @escaping (String) -> String, onChange: @escaping (String) -> Void ) -> some View { @@ -150,7 +155,7 @@ struct ContentView: View { Text(label) Spacer() NumericField( - placeholder: label, + placeholder: "", value: value, keyboardType: keyboardType, sanitize: sanitize, @@ -170,7 +175,7 @@ struct ContentView: View { private struct NumericField: View { let placeholder: String let value: String - let keyboardType: UIKeyboardType + let keyboardType: NumericFieldKeyboard let sanitize: (String) -> String let onChange: (String) -> Void var alignment: TextAlignment = .leading @@ -179,10 +184,15 @@ private struct NumericField: View { var body: some View { TextField(placeholder, text: $text) - .keyboardType(keyboardType) + #if os(iOS) + .keyboardType(keyboardType.uiKeyboardType) + #endif .multilineTextAlignment(alignment) + #if os(macOS) + .frame(maxWidth: 140) + #endif .onAppear { text = value } - .onChange(of: text) { _, newValue in + .onChange(of: text) { newValue in let sanitized = sanitize(newValue) if sanitized != newValue { text = sanitized @@ -191,7 +201,7 @@ private struct NumericField: View { onChange(sanitized) } } - .onChange(of: value) { _, newValue in + .onChange(of: value) { newValue in if newValue != text { text = newValue } @@ -216,3 +226,21 @@ private func sanitizeDecimalInput(_ raw: String) -> String { private func sanitizeIntegerInput(_ raw: String) -> String { 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 diff --git a/iosApp/iosApp/CurrencyPickerSheet.swift b/iosApp/iosApp/CurrencyPickerSheet.swift index 0be08b3..dac2ff9 100644 --- a/iosApp/iosApp/CurrencyPickerSheet.swift +++ b/iosApp/iosApp/CurrencyPickerSheet.swift @@ -10,7 +10,7 @@ struct CurrencyPickerSheet: View { @Environment(\.dismiss) private var dismiss var body: some View { - NavigationView { + NavigationStack { List(currencies, id: \.code) { info in Button { onToggle(info.code) @@ -27,13 +27,21 @@ struct CurrencyPickerSheet: View { } } .navigationTitle(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title)) + #if os(iOS) .navigationBarTitleDisplayMode(.inline) + #endif .toolbar { ToolbarItem(placement: .confirmationAction) { 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 { diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 927e0b9..e2d9040 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -5,6 +5,9 @@ struct iOSApp: App { var body: some Scene { WindowGroup { ContentView() + #if os(macOS) + .frame(minWidth: 420, minHeight: 500) + #endif } } } diff --git a/iosApp/iosApp/macOS.entitlements b/iosApp/iosApp/macOS.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/iosApp/iosApp/macOS.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 65478d1..09bc074 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -13,9 +13,10 @@ plugins { kotlin { listOf( iosArm64(), - iosSimulatorArm64() - ).forEach { iosTarget -> - iosTarget.binaries.framework { + iosSimulatorArm64(), + macosArm64() + ).forEach { appleTarget -> + appleTarget.binaries.framework { baseName = "Shared" isStatic = true export(libs.moko.resources) @@ -87,7 +88,7 @@ kotlin { implementation(libs.kotlinx.coroutinesTest) implementation(libs.ktor.client.mock) } - iosMain.dependencies { + appleMain.dependencies { implementation(libs.ktor.client.darwin) } jvmMain.dependencies { diff --git a/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt similarity index 100% rename from shared/src/iosMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt rename to shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt diff --git a/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.ios.kt b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.apple.kt similarity index 100% rename from shared/src/iosMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.ios.kt rename to shared/src/appleMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.apple.kt diff --git a/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.ios.kt b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.apple.kt similarity index 100% rename from shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.ios.kt rename to shared/src/appleMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.apple.kt diff --git a/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/IosLocalization.kt b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/ui/IosLocalization.kt similarity index 100% rename from shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/IosLocalization.kt rename to shared/src/appleMain/kotlin/xyz/tyiu/satsprice/ui/IosLocalization.kt diff --git a/shared/src/macosMain/kotlin/xyz/tyiu/satsprice/Platform.macos.kt b/shared/src/macosMain/kotlin/xyz/tyiu/satsprice/Platform.macos.kt new file mode 100644 index 0000000..3219074 --- /dev/null +++ b/shared/src/macosMain/kotlin/xyz/tyiu/satsprice/Platform.macos.kt @@ -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()