Implement proper iOS structure

This commit is contained in:
2025-12-06 12:17:40 +03:00
Unverified
parent 7696d26fd3
commit b95820de6c
36 changed files with 811 additions and 87 deletions
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="XcodeMetaData" PROJECT_DIR="$PROJECT_DIR$/iosApp" PROJECT_FILE="$PROJECT_DIR$/iosApp/iosApp.xcodeproj" />
</project>
+1 -1
View File
@@ -129,7 +129,7 @@ dependencies {
} }
compose.resources { compose.resources {
publicResClass = false publicResClass = true
packageOfResClass = "ru.fromchat" packageOfResClass = "ru.fromchat"
generateResClass = auto generateResClass = auto
} }
@@ -15,3 +15,4 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
@@ -14,3 +14,4 @@ object AndroidKtorLogger : Logger {
} }
} }
@@ -19,3 +19,4 @@ actual object Logger {
Log.e(tag, message, throwable) Log.e(tag, message, throwable)
} }
} }
@@ -1,23 +1,3 @@
<resources> <resources>
<string name="app_name">FromChat</string> <string name="app_name" translatable="false">FromChat</string>
<string name="register">Регистрация</string>
<string name="register_d">Создайте новый аккаунт</string>
<string name="register_button">Зарегистрироваться</string>
<string name="username">Имя пользователя</string>
<string name="password">Пароль</string>
<string name="confirm_password">Подтвердите пароль</string>
<string name="login">Войти</string>
<string name="login_d">Войдите в аккаунт</string>
<string name="welcome">Добро пожаловать!</string>
<string name="fill_all_fields">Пожалуйста, заполните все поля</string>
<string name="username_length_error">Имя пользователя должно быть от 3 до 20 символов</string>
<string name="password_length_error">Пароль должен быть от 5 до 50 символов</string>
<string name="passwords_dont_match">Пароли не совпадают</string>
<string name="chats">Чаты</string>
<string name="contacts">Контакты</string>
<string name="dms">ЛС</string>
<string name="coming_soon">Скоро будет...</string>
<string name="public_chat">Общий чат</string>
<string name="chat_last_mesaage">Вы: Последние сообщение</string>
<string name="message_placeholder">Введите сообщение</string>
</resources> </resources>
@@ -0,0 +1,44 @@
<resources>
<!-- App -->
<string name="app_name">FromChat</string>
<!-- Navigation -->
<string name="back">Back</string>
<string name="settings">Settings</string>
<string name="home">Home</string>
<string name="about">About app</string>
<!-- Authentication -->
<string name="welcome">Welcome!</string>
<string name="login">Sign In</string>
<string name="login_d">Sign in to your account</string>
<string name="register">Registration</string>
<string name="register_d">Create a new account</string>
<string name="register_button">Register</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="confirm_password">Confirm Password</string>
<string name="display_name">Display Name</string>
<string name="display_name_error">Display name must be between 1 and 64 characters</string>
<!-- Validation Errors -->
<string name="fill_all_fields">Please fill in all fields</string>
<string name="username_length_error">Username must be between 3 and 20 characters</string>
<string name="password_length_error">Password must be between 5 and 50 characters</string>
<string name="passwords_dont_match">Passwords do not match</string>
<!-- Main Screen -->
<string name="chats">Chats</string>
<string name="contacts">Contacts</string>
<string name="dms">Direct Messages</string>
<string name="coming_soon">Coming soon...</string>
<string name="public_chat">General Chat</string>
<string name="chat_last_mesaage">You: Last message</string>
<string name="message_placeholder">Type a message...</string>
<!-- Error Messages -->
<string name="error_unexpected">Unexpected error</string>
<string name="error_invalid_credentials">Invalid username or password</string>
<string name="error_connection">Connection error</string>
<string name="error_unknown">An unknown error occurred</string>
</resources>
@@ -5,6 +5,7 @@ import io.ktor.client.call.body
import io.ktor.client.plugins.ClientRequestException import io.ktor.client.plugins.ClientRequestException
suspend inline fun <Response> apiRequest( suspend inline fun <Response> apiRequest(
unexpectedError: String,
onError: (String, Exception) -> Unit = { _, _ -> }, onError: (String, Exception) -> Unit = { _, _ -> },
onSuccess: (Response) -> Unit = {}, onSuccess: (Response) -> Unit = {},
request: suspend () -> Response request: suspend () -> Response
@@ -17,7 +18,7 @@ suspend inline fun <Response> apiRequest(
val message = if (e.response.status.value in arrayOf(401, 403)) { val message = if (e.response.status.value in arrayOf(401, 403)) {
e.response.body<ErrorResponse>().detail e.response.body<ErrorResponse>().detail
} else { } else {
"Unexpected error" unexpectedError
} }
Logger.e("API", "API request failed: $message", e) Logger.e("API", "API request failed: $message", e)
@@ -26,7 +27,7 @@ suspend inline fun <Response> apiRequest(
return Result.failure(e) return Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("API", "API request failed: ${e.message}", e) Logger.e("API", "API request failed: ${e.message}", e)
onError("Unexpected error", e) onError(unexpectedError, e)
return Result.failure(e) return Result.failure(e)
} }
} }
@@ -1,6 +1,5 @@
package ru.fromchat.api package ru.fromchat.api
import ru.fromchat.core.Logger
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
import io.ktor.client.plugins.websocket.webSocket import io.ktor.client.plugins.websocket.webSocket
import io.ktor.client.request.url import io.ktor.client.request.url
@@ -10,6 +9,7 @@ import io.ktor.websocket.readText
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -20,6 +20,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.fromchat.WS_API_HOST import ru.fromchat.WS_API_HOST
import ru.fromchat.core.Logger
import kotlin.concurrent.Volatile
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
object WebSocketManager { object WebSocketManager {
@@ -41,7 +43,8 @@ object WebSocketManager {
} }
// State // State
@Volatile private var connecting: Boolean = false @Volatile
private var connecting: Boolean = false
@Volatile private var session: DefaultClientWebSocketSession? = null @Volatile private var session: DefaultClientWebSocketSession? = null
fun connect() { fun connect() {
@@ -6,3 +6,4 @@ expect object Logger {
fun w(tag: String, message: String, throwable: Throwable? = null) fun w(tag: String, message: String, throwable: Throwable? = null)
fun e(tag: String, message: String, throwable: Throwable? = null) fun e(tag: String, message: String, throwable: Throwable? = null)
} }
@@ -62,7 +62,10 @@ fun App() {
) { ) {
composable("login") { composable("login") {
LoginScreen( LoginScreen(
onLoginSuccess = { navController.navigate("chat") }, onLoginSuccess = {
navController.navigate("chat")
navController.popBackStack()
},
onNavigateToRegister = { navController.navigate("register") } onNavigateToRegister = { navController.navigate("register") }
) )
} }
@@ -24,22 +24,32 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.fromchat.R import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res
import ru.fromchat.api.ApiClient import ru.fromchat.api.ApiClient
import ru.fromchat.api.LoginRequest import ru.fromchat.api.LoginRequest
import ru.fromchat.api.apiRequest import ru.fromchat.api.apiRequest
import ru.fromchat.error_unexpected
import ru.fromchat.fill_all_fields
import ru.fromchat.login
import ru.fromchat.login_d
import ru.fromchat.password
import ru.fromchat.register_button
import ru.fromchat.ui.RowHeader import ru.fromchat.ui.RowHeader
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret import ru.fromchat.username
import ru.fromchat.welcome
@Composable @Composable
fun LoginScreen( fun LoginScreen(
onLoginSuccess: () -> Unit, onLoginSuccess: () -> Unit,
onNavigateToRegister: () -> Unit onNavigateToRegister: () -> Unit
) { ) {
val errorUnexpected = stringResource(Res.string.error_unexpected)
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding -> Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
var username by remember { mutableStateOf("") } var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
@@ -63,8 +73,8 @@ fun LoginScreen(
) { ) {
RowHeader( RowHeader(
icon = Icons.AutoMirrored.Filled.Login, icon = Icons.AutoMirrored.Filled.Login,
title = stringResource(R.string.welcome), title = stringResource(Res.string.welcome),
subtitle = stringResource(R.string.login_d) subtitle = stringResource(Res.string.login_d)
) )
if (alert != null) { if (alert != null) {
@@ -74,25 +84,25 @@ fun LoginScreen(
OutlinedTextField( OutlinedTextField(
value = username, value = username,
onValueChange = { username = it }, onValueChange = { username = it },
label = { Text(stringResource(R.string.username)) }, label = { Text(stringResource(Res.string.username)) },
singleLine = true singleLine = true
) )
OutlinedTextField( OutlinedTextField(
value = password, value = password,
onValueChange = { password = it }, onValueChange = { password = it },
label = { Text(stringResource(R.string.password)) }, label = { Text(stringResource(Res.string.password)) },
singleLine = true, singleLine = true,
visualTransformation = PasswordVisualTransformation() visualTransformation = PasswordVisualTransformation()
) )
FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) { FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
val alert_error_filling = stringResource(R.string.fill_all_fields) val alertErrorFilling = stringResource(Res.string.fill_all_fields)
Button( Button(
onClick = { onClick = {
if (username.isBlank() || password.isBlank()) { if (username.isBlank() || password.isBlank()) {
alert = alert_error_filling alert = alertErrorFilling
return@Button return@Button
} }
@@ -101,6 +111,7 @@ fun LoginScreen(
val derived = deriveAuthSecret(username.trim(), password.trim()) val derived = deriveAuthSecret(username.trim(), password.trim())
apiRequest( apiRequest(
unexpectedError = errorUnexpected,
onError = { message, _ -> onError = { message, _ ->
alert = message alert = message
}, },
@@ -111,11 +122,11 @@ fun LoginScreen(
} }
} }
) { ) {
Text(stringResource(R.string.login)) Text(stringResource(Res.string.login))
} }
Button(onClick = onNavigateToRegister) { Button(onClick = onNavigateToRegister) {
Text(stringResource(R.string.register_button)) Text(stringResource(Res.string.register_button))
} }
} }
} }
@@ -27,22 +27,38 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.fromchat.R import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res
import ru.fromchat.api.ApiClient import ru.fromchat.api.ApiClient
import ru.fromchat.api.RegisterRequest import ru.fromchat.api.RegisterRequest
import ru.fromchat.api.apiRequest import ru.fromchat.api.apiRequest
import ru.fromchat.back
import ru.fromchat.confirm_password
import ru.fromchat.display_name
import ru.fromchat.display_name_error
import ru.fromchat.error_unexpected
import ru.fromchat.fill_all_fields
import ru.fromchat.password
import ru.fromchat.password_length_error
import ru.fromchat.passwords_dont_match
import ru.fromchat.register
import ru.fromchat.register_button
import ru.fromchat.register_d
import ru.fromchat.ui.LocalNavController import ru.fromchat.ui.LocalNavController
import ru.fromchat.ui.RowHeader import ru.fromchat.ui.RowHeader
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret import ru.fromchat.username
import ru.fromchat.username_length_error
@Composable @Composable
fun RegisterScreen( fun RegisterScreen(
onRegistered: () -> Unit onRegistered: () -> Unit
) { ) {
val errorUnexpected = stringResource(Res.string.error_unexpected)
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding -> Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
var username by remember { mutableStateOf("") } var username by remember { mutableStateOf("") }
var displayName by remember { mutableStateOf("") } var displayName by remember { mutableStateOf("") }
@@ -69,8 +85,8 @@ fun RegisterScreen(
) { ) {
RowHeader( RowHeader(
icon = Icons.Filled.PersonAdd, icon = Icons.Filled.PersonAdd,
title = stringResource(R.string.register), title = stringResource(Res.string.register),
subtitle = stringResource(R.string.register_d) subtitle = stringResource(Res.string.register_d)
) )
if (alert != null) { if (alert != null) {
@@ -80,21 +96,21 @@ fun RegisterScreen(
OutlinedTextField( OutlinedTextField(
value = username, value = username,
onValueChange = { username = it }, onValueChange = { username = it },
label = { Text(stringResource(R.string.username)) }, label = { Text(stringResource(Res.string.username)) },
singleLine = true singleLine = true
) )
OutlinedTextField( OutlinedTextField(
value = displayName, value = displayName,
onValueChange = { displayName = it }, onValueChange = { displayName = it },
label = { Text("Display Name") }, label = { Text(stringResource(Res.string.display_name)) },
singleLine = true singleLine = true
) )
OutlinedTextField( OutlinedTextField(
value = password, value = password,
onValueChange = { password = it }, onValueChange = { password = it },
label = { Text(stringResource(R.string.password)) }, label = { Text(stringResource(Res.string.password)) },
singleLine = true, singleLine = true,
visualTransformation = PasswordVisualTransformation() visualTransformation = PasswordVisualTransformation()
) )
@@ -102,23 +118,24 @@ fun RegisterScreen(
OutlinedTextField( OutlinedTextField(
value = confirmPassword, value = confirmPassword,
onValueChange = { confirmPassword = it }, onValueChange = { confirmPassword = it },
label = { Text(stringResource(R.string.confirm_password)) }, label = { Text(stringResource(Res.string.confirm_password)) },
singleLine = true, singleLine = true,
visualTransformation = PasswordVisualTransformation() visualTransformation = PasswordVisualTransformation()
) )
FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) { FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
val alert_error_filling = stringResource(R.string.fill_all_fields) val alertErrorFilling = stringResource(Res.string.fill_all_fields)
val alert_error_password_little = stringResource(R.string.password_length_error) val alertErrorPasswordLittle = stringResource(Res.string.password_length_error)
val alert_error_name_little = stringResource(R.string.username_length_error) val alertErrorNameLittle = stringResource(Res.string.username_length_error)
val alert_error_password_confrim = stringResource(R.string.passwords_dont_match) val alertErrorPasswordConfrim = stringResource(Res.string.passwords_dont_match)
val alertErrorDisplayName = stringResource(Res.string.display_name_error)
IconButton( IconButton(
onClick = { navController.navigateUp() } onClick = { navController.navigateUp() }
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back" contentDescription = stringResource(Res.string.back)
) )
} }
@@ -126,23 +143,23 @@ fun RegisterScreen(
onClick = { onClick = {
// Checks // Checks
if (username.isBlank() || displayName.isBlank() || password.isBlank() || confirmPassword.isBlank()) { if (username.isBlank() || displayName.isBlank() || password.isBlank() || confirmPassword.isBlank()) {
alert = alert_error_filling alert = alertErrorFilling
return@Button return@Button
} }
if (password != confirmPassword) { if (password != confirmPassword) {
alert = alert_error_password_confrim alert = alertErrorPasswordConfrim
return@Button return@Button
} }
if (username.length !in 3..20) { if (username.length !in 3..20) {
alert = alert_error_name_little alert = alertErrorNameLittle
return@Button return@Button
} }
if (displayName.isBlank() || displayName.length > 64) { if (displayName.isBlank() || displayName.length > 64) {
alert = "Display name must be between 1 and 64 characters" alert = alertErrorDisplayName
return@Button return@Button
} }
if (password.length !in 5..50) { if (password.length !in 5..50) {
alert = alert_error_password_little alert = alertErrorPasswordLittle
return@Button return@Button
} }
@@ -151,6 +168,7 @@ fun RegisterScreen(
val derived = deriveAuthSecret(username.trim(), password) val derived = deriveAuthSecret(username.trim(), password)
apiRequest( apiRequest(
unexpectedError = errorUnexpected,
onError = { message, _ -> onError = { message, _ ->
alert = message alert = message
}, },
@@ -168,7 +186,7 @@ fun RegisterScreen(
} }
} }
) { ) {
Text(stringResource(R.string.register_button)) Text(stringResource(Res.string.register_button))
} }
} }
} }
@@ -11,9 +11,12 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import ru.fromchat.R import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res
import ru.fromchat.chat_last_mesaage
import ru.fromchat.chats
import ru.fromchat.public_chat
import ru.fromchat.ui.LocalNavController import ru.fromchat.ui.LocalNavController
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -28,7 +31,7 @@ fun ChatsTab() {
MediumTopAppBar( MediumTopAppBar(
title = { title = {
Text( Text(
text = stringResource(R.string.chats), text = stringResource(Res.string.chats),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
@@ -40,8 +43,8 @@ fun ChatsTab() {
LazyColumn(contentPadding = innerPadding) { LazyColumn(contentPadding = innerPadding) {
item { item {
ListItem( ListItem(
headlineContent = { Text(stringResource(R.string.public_chat)) }, headlineContent = { Text(stringResource(Res.string.public_chat)) },
supportingContent = { Text(stringResource(R.string.chat_last_mesaage)) }, supportingContent = { Text(stringResource(Res.string.chat_last_mesaage)) },
modifier = Modifier.clickable { modifier = Modifier.clickable {
navController.navigate("chats/publicChat") navController.navigate("chats/publicChat")
} }
@@ -22,10 +22,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import org.jetbrains.compose.resources.stringResource
import ru.fromchat.R import ru.fromchat.Res
import ru.fromchat.chats
import ru.fromchat.coming_soon
import ru.fromchat.contacts
import ru.fromchat.dms
import ru.fromchat.utils.exclude import ru.fromchat.utils.exclude
@Suppress("AssignedValueIsNeverRead")
@Composable @Composable
fun MainScreen() { fun MainScreen() {
var selectedTab by remember { mutableStateOf("chats") } var selectedTab by remember { mutableStateOf("chats") }
@@ -36,19 +41,19 @@ fun MainScreen() {
NavigationBarItem( NavigationBarItem(
selected = selectedTab == "chats", selected = selectedTab == "chats",
onClick = { selectedTab = "chats" }, onClick = { selectedTab = "chats" },
label = { Text(stringResource(R.string.chats)) }, label = { Text(stringResource(Res.string.chats)) },
icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) } icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) }
) )
NavigationBarItem( NavigationBarItem(
selected = selectedTab == "contacts", selected = selectedTab == "contacts",
onClick = { selectedTab = "contacts" }, onClick = { selectedTab = "contacts" },
label = { Text(stringResource(R.string.contacts)) }, label = { Text(stringResource(Res.string.contacts)) },
icon = { Icon(Icons.Filled.Contacts, contentDescription = null) } icon = { Icon(Icons.Filled.Contacts, contentDescription = null) }
) )
NavigationBarItem( NavigationBarItem(
selected = selectedTab == "dms", selected = selectedTab == "dms",
onClick = { selectedTab = "dms" }, onClick = { selectedTab = "dms" },
label = { Text(stringResource(R.string.dms)) }, label = { Text(stringResource(Res.string.dms)) },
icon = { Icon(Icons.Filled.Mail, contentDescription = null) } icon = { Icon(Icons.Filled.Mail, contentDescription = null) }
) )
} }
@@ -64,10 +69,10 @@ fun MainScreen() {
when (selectedTab) { when (selectedTab) {
"chats" -> ChatsTab() "chats" -> ChatsTab()
"contacts" -> { "contacts" -> {
Text(stringResource(R.string.coming_soon)) Text(stringResource(Res.string.coming_soon))
} }
"dms" -> { "dms" -> {
Text(stringResource(R.string.coming_soon)) Text(stringResource(Res.string.coming_soon))
} }
} }
} }
@@ -5,4 +5,5 @@ import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
@Suppress("NOTHING_TO_INLINE")
inline fun WindowInsets.exclude(sides: WindowInsetsSides) = exclude(this.only(sides)) inline fun WindowInsets.exclude(sides: WindowInsetsSides) = exclude(this.only(sides))
@@ -4,3 +4,4 @@ import ru.fromchat.ui.App
import androidx.compose.ui.window.ComposeUIViewController import androidx.compose.ui.window.ComposeUIViewController
fun MainViewController() = ComposeUIViewController { App() } fun MainViewController() = ComposeUIViewController { App() }
@@ -19,3 +19,4 @@ actual object Logger {
NSLog("ERROR: [%s] %s %s", tag, message, throwable?.message ?: "") NSLog("ERROR: [%s] %s %s", tag, message, throwable?.message ?: "")
} }
} }
+3
View File
@@ -0,0 +1,3 @@
TEAM_ID=82MWK23YNM
BUNDLE_ID=ru.fromchat.app
APP_NAME=FromChat
+407
View File
@@ -0,0 +1,407 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
7555FF7B242A565900829871 /* FromChat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FromChat.app; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
B92378962B6B1156000C7307 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
058557D7273AAEEB004C7B11 /* Preview Content */ = {
isa = PBXGroup;
children = (
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
42799AB246E5F90AF97AA0EF /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
7555FF72242A565900829871 = {
isa = PBXGroup;
children = (
AB1DB47929225F7C00F7AF9C /* Configuration */,
7555FF7D242A565900829871 /* iosApp */,
7555FF7C242A565900829871 /* Products */,
42799AB246E5F90AF97AA0EF /* Frameworks */,
);
sourceTree = "<group>";
};
7555FF7C242A565900829871 /* Products */ = {
isa = PBXGroup;
children = (
7555FF7B242A565900829871 /* FromChat.app */,
);
name = Products;
sourceTree = "<group>";
};
7555FF7D242A565900829871 /* iosApp */ = {
isa = PBXGroup;
children = (
058557BA273AAA24004C7B11 /* Assets.xcassets */,
7555FF82242A565900829871 /* ContentView.swift */,
7555FF8C242A565B00829871 /* Info.plist */,
2152FB032600AC8F00CF470E /* iOSApp.swift */,
058557D7273AAEEB004C7B11 /* Preview Content */,
);
path = iosApp;
sourceTree = "<group>";
};
AB1DB47929225F7C00F7AF9C /* Configuration */ = {
isa = PBXGroup;
children = (
AB3632DC29227652001CCB65 /* Config.xcconfig */,
);
path = Configuration;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
7555FF7A242A565900829871 /* iosApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
buildPhases = (
F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */,
7555FF77242A565900829871 /* Sources */,
B92378962B6B1156000C7307 /* Frameworks */,
7555FF79242A565900829871 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = iosApp;
packageProductDependencies = (
);
productName = iosApp;
productReference = 7555FF7B242A565900829871 /* FromChat.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7555FF73242A565900829871 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1130;
LastUpgradeCheck = 2610;
ORGANIZATIONNAME = orgName;
TargetAttributes = {
7555FF7A242A565900829871 = {
CreatedOnToolsVersion = 11.3.1;
};
};
};
buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 7555FF72242A565900829871;
packageReferences = (
);
productRefGroup = 7555FF7C242A565900829871 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
7555FF7A242A565900829871 /* iosApp */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
7555FF79242A565900829871 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Compile Kotlin Framework";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
7555FF77242A565900829871 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
7555FF83242A565900829871 /* ContentView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
7555FFA3242A565B00829871 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
7555FFA4242A565B00829871 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
7555FFA6242A565B00829871 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = 82MWK23YNM;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
);
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = FromChat;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
PRODUCT_NAME = "${APP_NAME}";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
7555FFA7242A565B00829871 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = 82MWK23YNM;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
);
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = FromChat;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
PRODUCT_NAME = "${APP_NAME}";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7555FFA3242A565B00829871 /* Debug */,
7555FFA4242A565B00829871 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7555FFA6242A565B00829871 /* Debug */,
7555FFA7242A565B00829871 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 7555FF73242A565900829871 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,5 @@
<?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 />
</plist>
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7555FF7A242A565900829871"
BuildableName = "FromChat.app"
BlueprintName = "iosApp"
ReferencedContainer = "container:iosApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7555FF7A242A565900829871"
BuildableName = "FromChat.app"
BlueprintName = "iosApp"
ReferencedContainer = "container:iosApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<LocationScenarioReference
identifier = "com.apple.dt.IDEFoundation.CurrentLocationScenarioIdentifier"
referenceType = "1">
</LocationScenarioReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,22 @@
<?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>SchemeUserState</key>
<dict>
<key>iOS.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>7555FF7A242A565900829871</key>
<dict>
<key>primary</key>
<true />
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "logo_square.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+18
View File
@@ -0,0 +1,18 @@
import UIKit
import SwiftUI
import ComposeApp
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
ComposeView()
.ignoresSafeArea(.all) // Compose has own keyboard handler
}
}
+50
View File
@@ -0,0 +1,50 @@
<?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>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
<key>UILaunchScreen</key>
<dict/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+10
View File
@@ -0,0 +1,10 @@
import SwiftUI
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@@ -1,23 +1,33 @@
package com.pr0gramm3r101.utils.crypto package com.pr0gramm3r101.utils.crypto
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ByteVar import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.get
import kotlinx.cinterop.reinterpret
import kotlinx.cinterop.usePinned import kotlinx.cinterop.usePinned
import platform.Foundation.NSData import platform.Foundation.NSData
import platform.Foundation.base64EncodedStringWithOptions
import platform.Foundation.create
actual object Base64 { actual object Base64 {
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
actual fun encode(bytes: ByteArray): String { actual fun encode(bytes: ByteArray): String {
return bytes.usePinned { pinned -> return bytes.usePinned { pinned ->
val nsData = NSData.create(bytes = pinned.addressOf(0), length = bytes.size.toULong()) val nsData = NSData.create(bytes = pinned.addressOf(0), length = bytes.size.toULong())
nsData.base64EncodedStringWithOptions(0u) nsData.base64EncodedStringWithOptions(0u)
} }
} }
@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class)
actual fun decode(base64: String): ByteArray { actual fun decode(base64: String): ByteArray {
val nsData = NSData.create(base64EncodedString = base64, options = 0u) ?: return ByteArray(0) val nsData = NSData.create(base64EncodedString = base64, options = 0u) ?: return ByteArray(0)
val length = nsData.length.toInt() val length = nsData.length.toInt()
val bytes = nsData.bytes ?: return ByteArray(0) val bytesPtr = nsData.bytes ?: return ByteArray(0)
val bytePtr = bytesPtr.reinterpret<ByteVar>()
return ByteArray(length) { index -> return ByteArray(length) { index ->
bytes.reinterpret<ByteVar>()[index] bytePtr[index]
} }
} }
} }
@@ -1,11 +1,15 @@
package com.pr0gramm3r101.utils.crypto package com.pr0gramm3r101.utils.crypto
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import platform.CommonCrypto.CCHmac import platform.CoreCrypto.CCHmac
import platform.CommonCrypto.kCCHmacAlgSHA256 import platform.CoreCrypto.kCCHmacAlgSHA256
actual object Hmac { actual object Hmac {
@OptIn(ExperimentalForeignApi::class)
actual suspend fun hmacSha256(key: ByteArray, data: ByteArray): ByteArray = withContext(Dispatchers.Default) { actual suspend fun hmacSha256(key: ByteArray, data: ByteArray): ByteArray = withContext(Dispatchers.Default) {
val result = ByteArray(32) val result = ByteArray(32)
@@ -3,11 +3,12 @@ package com.pr0gramm3r101.utils.settings
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import platform.Foundation.NSUserDefaults import platform.Foundation.NSUserDefaults
import platform.Foundation.NSBundle
class IosAsyncSettings : AsyncSettings { class IosAsyncSettings : AsyncSettings {
private val defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults private val defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults
override suspend fun putString(key: String, value: String) = withContext(Dispatchers.Default) { override suspend fun putString(key: String, value: String): Unit = withContext(Dispatchers.Default) {
defaults.setObject(value, key) defaults.setObject(value, key)
defaults.synchronize() defaults.synchronize()
} }
@@ -16,7 +17,7 @@ class IosAsyncSettings : AsyncSettings {
defaults.stringForKey(key) ?: default defaults.stringForKey(key) ?: default
} }
override suspend fun putInt(key: String, value: Int) = withContext(Dispatchers.Default) { override suspend fun putInt(key: String, value: Int): Unit = withContext(Dispatchers.Default) {
defaults.setInteger(value.toLong(), key) defaults.setInteger(value.toLong(), key)
defaults.synchronize() defaults.synchronize()
} }
@@ -26,7 +27,7 @@ class IosAsyncSettings : AsyncSettings {
if (value != null) value.toInt() else default if (value != null) value.toInt() else default
} }
override suspend fun putLong(key: String, value: Long) = withContext(Dispatchers.Default) { override suspend fun putLong(key: String, value: Long): Unit = withContext(Dispatchers.Default) {
defaults.setObject(value, key) defaults.setObject(value, key)
defaults.synchronize() defaults.synchronize()
} }
@@ -35,7 +36,7 @@ class IosAsyncSettings : AsyncSettings {
(defaults.objectForKey(key) as? platform.Foundation.NSNumber)?.longValue ?: default (defaults.objectForKey(key) as? platform.Foundation.NSNumber)?.longValue ?: default
} }
override suspend fun putFloat(key: String, value: Float) = withContext(Dispatchers.Default) { override suspend fun putFloat(key: String, value: Float): Unit = withContext(Dispatchers.Default) {
defaults.setFloat(value, key) defaults.setFloat(value, key)
defaults.synchronize() defaults.synchronize()
} }
@@ -45,7 +46,7 @@ class IosAsyncSettings : AsyncSettings {
if (value != null) value.toFloat() else default if (value != null) value.toFloat() else default
} }
override suspend fun putBoolean(key: String, value: Boolean) = withContext(Dispatchers.Default) { override suspend fun putBoolean(key: String, value: Boolean): Unit = withContext(Dispatchers.Default) {
defaults.setBool(value, key) defaults.setBool(value, key)
defaults.synchronize() defaults.synchronize()
} }
@@ -58,7 +59,7 @@ class IosAsyncSettings : AsyncSettings {
} }
} }
override suspend fun putStringSet(key: String, value: Set<String>) = withContext(Dispatchers.Default) { override suspend fun putStringSet(key: String, value: Set<String>): Unit = withContext(Dispatchers.Default) {
defaults.setObject(value.toList(), key) defaults.setObject(value.toList(), key)
defaults.synchronize() defaults.synchronize()
} }
@@ -68,7 +69,7 @@ class IosAsyncSettings : AsyncSettings {
list.filterIsInstance<String>().toSet() list.filterIsInstance<String>().toSet()
} }
override suspend fun putStringList(key: String, value: List<String>) = withContext(Dispatchers.Default) { override suspend fun putStringList(key: String, value: List<String>): Unit = withContext(Dispatchers.Default) {
defaults.setObject(value, key) defaults.setObject(value, key)
defaults.synchronize() defaults.synchronize()
} }
@@ -78,7 +79,7 @@ class IosAsyncSettings : AsyncSettings {
list.filterIsInstance<String>() list.filterIsInstance<String>()
} }
override suspend fun remove(key: String) = withContext(Dispatchers.Default) { override suspend fun remove(key: String): Unit = withContext(Dispatchers.Default) {
defaults.removeObjectForKey(key) defaults.removeObjectForKey(key)
defaults.synchronize() defaults.synchronize()
} }
@@ -87,8 +88,9 @@ class IosAsyncSettings : AsyncSettings {
defaults.objectForKey(key) != null defaults.objectForKey(key) != null
} }
override suspend fun clear() = withContext(Dispatchers.Default) { override suspend fun clear(): Unit = withContext(Dispatchers.Default) {
val domain = defaults.persistentDomainForName(defaults.bundleIdentifier() ?: "") val bundleId = NSBundle.mainBundle.bundleIdentifier ?: ""
val domain = defaults.persistentDomainForName(bundleId)
domain?.keys?.forEach { key -> domain?.keys?.forEach { key ->
defaults.removeObjectForKey(key as String) defaults.removeObjectForKey(key as String)
} }
@@ -96,4 +98,4 @@ class IosAsyncSettings : AsyncSettings {
} }
} }
actual val asyncSettings: AsyncSettings = IosAsyncSettings() actual val asyncSettings: AsyncSettings = IosAsyncSettings()