From 8ebaecf9028eb5b76aae1123bd62cd47342151f4 Mon Sep 17 00:00:00 2001 From: cheykrym Date: Wed, 11 Jun 2025 07:54:16 +0300 Subject: [PATCH] add login and register --- Shared/Network/AuthService.swift | 291 +++++++++++++++++- Shared/Network/refreshtokenex.swift | 89 ++++++ Shared/Resources/en.lproj/Localizable.strings | 51 ++- Shared/Resources/ru.lproj/Localizable.strings | 46 ++- Shared/ViewModels/LoginViewModel.swift | 61 +++- Shared/Views/CustomTextField.swift | 55 ++++ Shared/Views/LoginView.swift | 181 ++++++----- Shared/Views/MainView.swift | 8 +- Shared/Views/RegistrationView.swift | 243 ++++++++++----- Shared/Views/SettingsTab.swift | 11 + Shared/config.swift | 5 +- Shared/volnahubApp.swift | 2 +- iOS/Info.plist | 2 +- volnahub.xcodeproj/project.pbxproj | 20 +- .../xcschemes/xcschememanagement.plist | 4 +- 15 files changed, 869 insertions(+), 200 deletions(-) create mode 100644 Shared/Network/refreshtokenex.swift create mode 100644 Shared/Views/CustomTextField.swift diff --git a/Shared/Network/AuthService.swift b/Shared/Network/AuthService.swift index 92e140a..9d0cdd9 100644 --- a/Shared/Network/AuthService.swift +++ b/Shared/Network/AuthService.swift @@ -8,22 +8,287 @@ import Foundation class AuthService { - func autoLogin(completion: @escaping (Bool) -> Void) { - // Симуляция проверки токена - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { - let success = false // Пока всегда неуспешно, для теста - completion(success) - } - } - func login(username: String, password: String, completion: @escaping (Bool, String?) -> Void) { - // Симуляция запроса - DispatchQueue.global().asyncAfter(deadline: .now() + 2) { - if username == "test" && password == "123123" { + func autoLogin(completion: @escaping (Bool, String?) -> Void) { + // 1️⃣ Проверяем наличие текущего пользователя + if let currentUser = UserDefaults.standard.string(forKey: "currentUser"), + let accessToken = KeychainService.shared.get(forKey: "access_token", service: currentUser), + let refreshToken = KeychainService.shared.get(forKey: "refresh_token", service: currentUser) { + if AppConfig.DEBUG{ print("AutoLogin: найден текущий пользователь — \(currentUser)")} + completion(true, nil) + return + } + + // 2️⃣ Текущий пользователь не найден или токены отсутствуют + if AppConfig.DEBUG{ print("AutoLogin: текущий пользователь не найден или токены отсутствуют. Пробуем найти другого пользователя...")} + + let allUsers = KeychainService.shared.getAllServices() + + for user in allUsers { + let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil + let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil + + if hasAccessToken && hasRefreshToken { + // Нашли пользователя с токенами — назначаем как currentUser + UserDefaults.standard.set(user, forKey: "currentUser") + if AppConfig.DEBUG{ print("AutoLogin: переключились на пользователя \(user)")} completion(true, nil) - } else { - completion(false, "Неверные учетные данные.") + return } } + + // 3️⃣ Если никто не найден +// completion(false, "Не найден авторизованный пользователь. Пожалуйста, войдите снова.") + completion(false, nil) + } + + + func login(username: String, password: String, completion: @escaping (Bool, String?) -> Void) { + let url = URL(string: "\(AppConfig.API_SERVER)/auth/login")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("\(AppConfig.USER_AGENT)", forHTTPHeaderField: "User-Agent") + + let payload: [String: String] = [ + "login": username, + "password": password + ] + + do { + let jsonData = try JSONEncoder().encode(payload) + request.httpBody = jsonData + } catch { + DispatchQueue.main.async { + completion(false, NSLocalizedString("AuthService_error_serialization", comment: "")) + } + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + DispatchQueue.main.async { + if let error = error { + let errorMessage = String(format: NSLocalizedString("AuthService_error_network", comment: ""), error.localizedDescription) + completion(false, errorMessage) + return + } + + guard let httpResponse = response as? HTTPURLResponse else { + completion(false, NSLocalizedString("AuthService_error_invalid_response", comment: "")) + return + } + + guard (200...299).contains(httpResponse.statusCode) else { + if httpResponse.statusCode == 401{ + completion(false, NSLocalizedString("AuthService_error_invalid_credentials", comment: "")) + } else if httpResponse.statusCode == 502{ + completion(false, NSLocalizedString("AuthService_error_server_unavailable", comment: "")) + } else if httpResponse.statusCode == 429 { + completion(false, NSLocalizedString("AuthService_error_too_many_requests", comment: "")) + } else { + let errorMessage = String(format: NSLocalizedString("AuthService_error_server_error", comment: ""), "\(httpResponse.statusCode)") + completion(false, errorMessage) + } + return + } + + guard let data = data else { + completion(false, NSLocalizedString("AuthService_error_empty_response", comment: "")) + return + } + + do { + let decoder = JSONDecoder() + let loginResponse = try decoder.decode(LoginResponse.self, from: data) + + // Сохраняем токены в Keychain + KeychainService.shared.save(loginResponse.access_token, forKey: "access_token", service: username) + KeychainService.shared.save(loginResponse.refresh_token, forKey: "refresh_token", service: username) + UserDefaults.standard.set(username, forKey: "currentUser") + + completion(true, nil) + } catch { + completion(false, NSLocalizedString("AuthService_error_parsing_response", comment: "")) + } + } + } + task.resume() + } + + func register(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) { + let url = URL(string: "\(AppConfig.API_SERVER)/auth/register")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(AppConfig.USER_AGENT, forHTTPHeaderField: "User-Agent") + + let payload: [String: Any] = [ + "login": username, + "password": password, + "invite": invite ?? NSNull() + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: payload) + request.httpBody = jsonData + } catch { + DispatchQueue.main.async { + completion(false, NSLocalizedString("AuthService_error_serialization", comment: "")) + } + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + DispatchQueue.main.async { + if let error = error { + let errorMessage = String(format: NSLocalizedString("AuthService_error_network", comment: ""), error.localizedDescription) + completion(false, errorMessage) + return + } + + guard let httpResponse = response as? HTTPURLResponse else { + completion(false, NSLocalizedString("AuthService_error_invalid_response", comment: "")) + return + } + + guard let data = data else { + completion(false, NSLocalizedString("AuthService_error_empty_response", comment: "")) + return + } + + let decoder = JSONDecoder() + + if (200...299).contains(httpResponse.statusCode) { + do { + let _ = try decoder.decode(RegisterResponse.self, from: data) + if AppConfig.DEBUG{ print("Регистрация успешна. Пытаемся сразу войти...")} + + // Сразу логинимся + self.login(username: username, password: password) { loginSuccess, loginMessage in + if loginSuccess { + completion(true, "Регистрация и вход выполнены успешно.") + } else { + // Регистрация успешна, но логин не удался — покажем сообщение + completion(false, loginMessage ?? NSLocalizedString("AuthService_login_success_but_failed", comment: "")) + } + } + } catch { + completion(false, NSLocalizedString("AuthService_error_parsing_response", comment: "")) + } + } else { + // Ошибка сервера — пробуем распарсить message + if let errorResponseMessage = try? decoder.decode(ErrorResponseMessage.self, from: data), + let message = errorResponseMessage.message { + + if let jsonString = String(data: data, encoding: .utf8) { + if AppConfig.DEBUG{ print("Raw JSON:", jsonString)} + } + if AppConfig.DEBUG{ print("message:", message)} + + if httpResponse.statusCode == 400 { + if message.contains("Invalid invitation code") { + completion(false, NSLocalizedString("AuthService_error_invalid_invitation_code", comment: "")) + } else if message.contains("This invitation is not active") { + completion(false, NSLocalizedString("AuthService_error_invitation_not_active", comment: "")) + } else if message.contains("This invitation has reached its usage limit") { + completion(false, NSLocalizedString("AuthService_error_invitation_usage_limit", comment: "")) + } else if message.contains("This invitation has expired") { + completion(false, NSLocalizedString("AuthService_error_invitation_expired", comment: "")) + } else if message.contains("Login already registered") { + completion(false, NSLocalizedString("AuthService_error_login_already_registered", comment: "")) + } else { + completion(false, message) + } + } else if httpResponse.statusCode == 403 { + if message.contains("Registration is currently disabled") { + completion(false, NSLocalizedString("AuthService_error_registration_disabled", comment: "")) + } else { + completion(false, message) + } + } else if httpResponse.statusCode == 429 { + completion(false, NSLocalizedString("AuthService_error_too_many_requests", comment: "")) + } else if httpResponse.statusCode == 502{ + completion(false, NSLocalizedString("AuthService_error_server_unavailable", comment: "")) + } else { + let errorMessage = String(format: NSLocalizedString("AuthService_error_server_error", comment: ""), "\(httpResponse.statusCode)") + completion(false, errorMessage) + } + } else { + // Не удалось распарсить JSON — fallback + if httpResponse.statusCode == 400 { + completion(false, NSLocalizedString("AuthService_error_invalid_request", comment: "")) + } else if httpResponse.statusCode == 403 { + completion(false, NSLocalizedString("AuthService_error_registration_forbidden", comment: "")) + } else if httpResponse.statusCode == 429 { + completion(false, NSLocalizedString("AuthService_error_too_many_requests", comment: "")) + } else if httpResponse.statusCode == 502{ + completion(false, NSLocalizedString("AuthService_error_server_unavailable", comment: "")) + } else { + let errorMessage = String(format: NSLocalizedString("AuthService_error_server_error", comment: ""), "\(httpResponse.statusCode)") + completion(false, errorMessage) + } + } + } + } + } + task.resume() + } + + + + + func logoutCurrentUser(completion: @escaping (Bool, String?) -> Void) { + guard let currentUser = UserDefaults.standard.string(forKey: "currentUser") else { + completion(false, "Не найден текущий пользователь.") + return + } + + // Удаляем токены текущего пользователя + KeychainService.shared.delete(forKey: "access_token", service: currentUser) + KeychainService.shared.delete(forKey: "refresh_token", service: currentUser) + + // Сбрасываем текущего пользователя + UserDefaults.standard.removeObject(forKey: "currentUser") + + // Пробуем переключиться на другого пользователя + let allUsers = KeychainService.shared.getAllServices() + for user in allUsers { + let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil + let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil + + if hasAccessToken && hasRefreshToken { + UserDefaults.standard.set(user, forKey: "currentUser") + if AppConfig.DEBUG{ print("Logout: переключились на пользователя \(user)")} + completion(true, nil) + return + } + } + + // Если пользователей больше нет +// completion(false, "Нет доступных пользователей. Пожалуйста, войдите снова.") + completion(false, nil) } } + +struct LoginResponse: Decodable { + let status: String + let access_token: String + let refresh_token: String + let token_type: String +} + +struct TokenRefreshResponse: Decodable { + let status: String + let access_token: String + let token_type: String +} + +struct RegisterResponse: Decodable { + let status: String + let message: String +} + +struct ErrorResponseMessage: Decodable { + let status: String? + let message: String? +} diff --git a/Shared/Network/refreshtokenex.swift b/Shared/Network/refreshtokenex.swift new file mode 100644 index 0000000..7852ec8 --- /dev/null +++ b/Shared/Network/refreshtokenex.swift @@ -0,0 +1,89 @@ +//// +//// refreshtokenex.swift +//// volnahub (iOS) +//// +//// Created by cheykrym on 11/06/2025. +//// +// +//import Foundation +// +//func autoLogin(completion: @escaping (Bool, String?) -> Void) { +// guard let username = UserDefaults.standard.string(forKey: "currentUser") else { +// completion(false, "Не найден текущий пользователь") +// return +// } +// +// guard let accessToken = KeychainService.shared.get(forKey: "access_token", service: username), +// let refreshToken = KeychainService.shared.get(forKey: "refresh_token", service: username) else { +// completion(false, "Токены отсутствуют. Пожалуйста, войдите снова.") +// return +// } +// +//// let url = URL(string: "\(AppConfig.API_SERVER)/auth/refresh")! +//// var request = URLRequest(url: url) +//// request.httpMethod = "POST" +//// request.setValue("application/json", forHTTPHeaderField: "Content-Type") +////// request.setValue("VolnahubApp", forHTTPHeaderField: "User-Agent") +//// request.setValue("VolnahubApp", forHTTPHeaderField: "\(AppConfig.USER_AGENT)") +//// +//// let payload: [String: String] = [ +//// "access_token": accessToken, +//// "refresh_token": refreshToken +//// ] +//// +//// do { +//// let jsonData = try JSONEncoder().encode(payload) +//// request.httpBody = jsonData +//// } catch { +//// DispatchQueue.main.async { +//// completion(false, "Не удалось подготовить запрос.") +//// } +//// return +//// } +//// +//// let task = URLSession.shared.dataTask(with: request) { data, response, error in +//// DispatchQueue.main.async { +//// if let error = error { +//// print("AutoLogin error: \(error.localizedDescription)") +//// completion(false, "Ошибка сети: \(error.localizedDescription)") +//// return +//// } +//// +//// guard let httpResponse = response as? HTTPURLResponse else { +//// completion(false, "Некорректный ответ от сервера.") +//// return +//// } +//// +//// guard (200...299).contains(httpResponse.statusCode) else { +//// if httpResponse.statusCode == 401 { +//// print("деавторизация") //TODO +//// completion(false, "Сессия недействительна. Пожалуйста, войдите снова.") +//// } else if httpResponse.statusCode == 502 { +//// completion(false, "Сервер не отвечает. Попробуйте позже.") +//// } else { +//// completion(false, "Ошибка сервера: \(httpResponse.statusCode)") +//// } +//// return +//// } +//// +//// guard let data = data else { +//// completion(false, "Пустой ответ от сервера.") +//// return +//// } +//// +//// do { +//// let decoder = JSONDecoder() +//// let refreshResponse = try decoder.decode(TokenRefreshResponse.self, from: data) +//// +//// KeychainService.shared.save(refreshResponse.access_token, forKey: "access_token", service: username) +//// +//// print("AutoLogin: токен обновлён.") +//// completion(true, nil) +//// } catch { +//// print("AutoLogin decode error: \(error.localizedDescription)") +//// completion(false, "Ошибка обработки ответа сервера.") +//// } +//// } +//// } +//// task.resume() +//} diff --git a/Shared/Resources/en.lproj/Localizable.strings b/Shared/Resources/en.lproj/Localizable.strings index b9cd02e..b0a37e3 100644 --- a/Shared/Resources/en.lproj/Localizable.strings +++ b/Shared/Resources/en.lproj/Localizable.strings @@ -1,7 +1,54 @@ -/* +/* Localizable.strings volnahub Created by cheykrym on 10/06/2025. - */ + +/* General */ +"ok" = "OK"; +"loading_placeholder" = "Loading..."; + +/* LoginView */ +"LoginView_change_language" = "Language"; +"LoginView_login" = "Login"; +"LoginView_password" = "Password"; +"LoginView_button_login" = "Log in"; +"LoginView_error" = "Login error"; +"LoginView_button_register" = "Register"; +"LoginView_error_username_invalid" = "Username must be 3 to 32 characters (letters, digits, or _)"; +"LoginView_error_password_invalid" = "Password must be 6 to 32 characters long"; + +/* RegistrationView */ +"RegistrationView_title" = "Registration"; +"RegistrationView_fullname" = "Full name"; +"RegistrationView_login" = "Login"; +"RegistrationView_error_username_invalid" = "Username must be 3 to 32 characters (letters, digits, or _)"; +"RegistrationView_password" = "Password"; +"RegistrationView_error_password_invalid" = "Password must be 6 to 32 characters long"; +"RegistrationView_confirm_password" = "Confirm password"; +"RegistrationView_error_confirm_password_invalid" = "Passwords do not match"; +"RegistrationView_invite" = "Invite code (optional)"; +"RegistrationView_button_register" = "Register"; +"RegistrationView_close" = "Close"; +"RegistrationView_error" = "Registration error"; + +/* AuthService */ +"AuthService_error_invalid_invitation_code" = "Invalid invitation code."; +"AuthService_error_invitation_not_active" = "The invitation is not active."; +"AuthService_error_invitation_usage_limit" = "The invitation has reached its usage limit."; +"AuthService_error_invitation_expired" = "The invitation has expired."; +"AuthService_error_login_already_registered" = "This login is already registered."; +"AuthService_error_registration_disabled" = "Registration is temporarily unavailable."; +"AuthService_error_server_unavailable" = "Server is unavailable. Please try again later."; +"AuthService_error_too_many_requests" = "Too many requests."; +"AuthService_error_invalid_request" = "Invalid request (400)."; +"AuthService_error_registration_forbidden" = "Registration is forbidden."; +"AuthService_error_server_error" = "Server error: %@"; +"AuthService_error_network" = "Network error: %@"; +"AuthService_error_invalid_response" = "Invalid server response."; +"AuthService_error_invalid_credentials" = "Invalid username or password."; +"AuthService_error_empty_response" = "Empty server response."; +"AuthService_error_parsing_response" = "Failed to parse server response."; +"AuthService_error_serialization" = "Failed to serialize request data."; +"AuthService_login_success_but_failed" = "Registration succeeded, but login failed."; diff --git a/Shared/Resources/ru.lproj/Localizable.strings b/Shared/Resources/ru.lproj/Localizable.strings index 70d9123..9c0803d 100644 --- a/Shared/Resources/ru.lproj/Localizable.strings +++ b/Shared/Resources/ru.lproj/Localizable.strings @@ -5,10 +5,54 @@ Created by cheykrym on 10/06/2025. */ +"ok" = "OK"; "loading_placeholder" = "Загрузка..."; + +/* LoginView */ "LoginView_change_language" = "Язык"; -"LoginView_title" = "Вход"; "LoginView_login" = "Логин"; "LoginView_password" = "Пароль"; "LoginView_button_login" = "Войти"; +"LoginView_error" = "Ошибка авторизации"; "LoginView_button_register" = "Регистрация"; +"LoginView_error_username_invalid" = "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)"; +"LoginView_error_password_invalid" = "Пароль должен быть от 6 до 32 символов"; + +/* RegistrationView */ +"RegistrationView_title" = "Регистрация"; +"RegistrationView_fullname" = "Имя"; +"RegistrationView_login" = "Логин"; +"RegistrationView_error_username_invalid" = "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)"; +"RegistrationView_password" = "Пароль"; +"RegistrationView_error_password_invalid" = "Пароль должен быть от 6 до 32 символов"; +"RegistrationView_confirm_password" = "Подтверждение пароля"; +"RegistrationView_error_confirm_password_invalid" = "Пароли не совпадают"; +"RegistrationView_invite" = "Инвайт-код (необязательно)"; +"RegistrationView_button_register" = "Зарегистрироваться"; +"RegistrationView_close" = "Закрыть"; +"RegistrationView_error" = "Ошибка регистрация"; + +/* AuthService */ +"AuthService_error_invalid_invitation_code" = "Неверный код приглашения."; +"AuthService_error_invitation_not_active" = "Приглашение не активно."; +"AuthService_error_invitation_usage_limit" = "Приглашение достигло лимита использования."; +"AuthService_error_invitation_expired" = "Приглашение истекло."; +"AuthService_error_login_already_registered" = "Логин уже занят."; +"AuthService_error_registration_disabled" = "Регистрация временно недоступна."; +"AuthService_error_server_unavailable" = "Сервер не отвечает. Попробуйте позже."; +"AuthService_error_too_many_requests" = "Слишком много запросов."; +"AuthService_error_invalid_request" = "Неверный запрос (400)."; +"AuthService_error_registration_forbidden" = "Регистрация запрещена."; +"AuthService_error_server_error" = "Ошибка сервера: %@"; +"AuthService_error_network" = "Ошибка сети: %@"; +"AuthService_error_invalid_response" = "Некорректный ответ от сервера."; +"AuthService_error_invalid_credentials" = "Неверный логин или пароль."; +"AuthService_error_empty_response" = "Пустой ответ от сервера."; +"AuthService_error_parsing_response" = "Не удалось обработать ответ сервера."; +"AuthService_error_serialization" = "Не удалось сериализовать данные запроса."; +"AuthService_login_success_but_failed" = "Регистрация выполнена, но вход не удался."; + +/* MainView */ +"MainView_contacts" = "Контакты"; +"MainView_chats" = "Чаты"; +"MainView_settings" = "Настройки"; diff --git a/Shared/ViewModels/LoginViewModel.swift b/Shared/ViewModels/LoginViewModel.swift index 8348f96..ad32f91 100644 --- a/Shared/ViewModels/LoginViewModel.swift +++ b/Shared/ViewModels/LoginViewModel.swift @@ -11,28 +11,39 @@ import Combine class LoginViewModel: ObservableObject { @Published var username: String = "" @Published var password: String = "" - @Published var isLoading: Bool = true // сразу true, чтобы вызывался автологин + @Published var isLoading: Bool = true // сразу true, чтобы показать спиннер при автологине @Published var showError: Bool = false @Published var errorMessage: String = "" @Published var isLoggedIn: Bool = false private let authService = AuthService() - + init() { + // Если username сохранён, подставим его сразу + if let savedUsername = UserDefaults.standard.string(forKey: "currentUser") { + username = savedUsername + } + + // Запускаем автологин autoLogin() } - + func autoLogin() { - authService.autoLogin { [weak self] success in + authService.autoLogin { [weak self] success, error in DispatchQueue.main.async { self?.isLoading = false if success { self?.isLoggedIn = true + } else { + self?.isLoggedIn = false + self?.errorMessage = error ?? "Произошла ошибка." + self?.showError = false } } } } - + + func login() { isLoading = true showError = false @@ -50,10 +61,42 @@ class LoginViewModel: ObservableObject { } } - func logout() { - username = "" - password = "" - isLoggedIn = false + func registerUser(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) { + authService.register(username: username, password: password, invite: invite) { [weak self] success, message in + DispatchQueue.main.async { + if success { + self?.isLoggedIn = true // 👈 переключаем на главный экран после автологина + } + completion(success, message) + } + } } + func logoutCurrentUser() { + authService.logoutCurrentUser { [weak self] success, error in + DispatchQueue.main.async { + if success { + self?.username = UserDefaults.standard.string(forKey: "currentUser") ?? "" + self?.password = "" + self?.isLoggedIn = true + self?.showError = false + } else { + self?.username = "" + self?.password = "" + self?.isLoggedIn = false + self?.errorMessage = error ?? "Ошибка при деавторизации." + self?.showError = false + } + } + } + } + + +// func logout() { +// username = "" +// password = "" +// isLoggedIn = false +// showError = false +// errorMessage = "" +// } } diff --git a/Shared/Views/CustomTextField.swift b/Shared/Views/CustomTextField.swift new file mode 100644 index 0000000..f249cd1 --- /dev/null +++ b/Shared/Views/CustomTextField.swift @@ -0,0 +1,55 @@ +// +// CustomTextField.swift +// VolnahubApp +// +// Created by cheykrym on 11/06/2025. +// + +import SwiftUI + +struct CustomTextField: UIViewRepresentable { + class Coordinator: NSObject, UITextFieldDelegate { + var parent: CustomTextField + + init(_ parent: CustomTextField) { + self.parent = parent + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + parent.onReturn() + return false // предотвращаем автоматический переход на новую строку + } + + @objc func textFieldDidChange(_ textField: UITextField) { + parent.text = textField.text ?? "" + } + } + + var placeholder: String + @Binding var text: String + var isSecure: Bool = false + var onReturn: () -> Void + + func makeUIView(context: Context) -> UITextField { + let textField = UITextField() + textField.placeholder = placeholder + textField.delegate = context.coordinator + textField.borderStyle = .roundedRect + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.isSecureTextEntry = isSecure + textField.returnKeyType = .next + textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged) + return textField + } + + func updateUIView(_ uiView: UITextField, context: Context) { + if uiView.text != text { + uiView.text = text + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } +} diff --git a/Shared/Views/LoginView.swift b/Shared/Views/LoginView.swift index ba1ff8b..936b6fd 100644 --- a/Shared/Views/LoginView.swift +++ b/Shared/Views/LoginView.swift @@ -24,115 +24,128 @@ struct LoginView: View { var body: some View { - VStack { - HStack { - - Button(action: openLanguageSettings) { - Text("🌍 " + NSLocalizedString("LoginView_change_language", comment: "")) - .padding() + ZStack { + Color.clear // чтобы поймать тап + .contentShape(Rectangle()) + .onTapGesture { + hideKeyboard() } - Spacer() - Button(action: toggleTheme) { - Image(systemName: isDarkMode ? "moon.fill" : "sun.max.fill") - .padding() - } - } - Spacer() + VStack { + HStack { - -// Text(NSLocalizedString("LoginView_title", comment: "")) -// .font(.largeTitle) -// .bold() - - TextField(NSLocalizedString("LoginView_login", comment: ""), text: $viewModel.username) - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - .autocapitalization(.none) - .disableAutocorrection(true) - .onChange(of: viewModel.username) { newValue in - if newValue.count > 32 { - viewModel.username = String(newValue.prefix(32)) + Button(action: openLanguageSettings) { + Text("🌍") + .padding() + } + Spacer() + Button(action: toggleTheme) { + Image(systemName: isDarkMode ? "moon.fill" : "sun.max.fill") + .padding() } } - - // Показываем ошибку для логина - if !isUsernameValid && !viewModel.username.isEmpty { - Text(NSLocalizedString("LoginView_error_username_invalid", comment: "Неверный логин")) - .foregroundColor(.red) - .font(.caption) + .onTapGesture { + hideKeyboard() } - // Показываем поле пароля (даже если оно невалидное) только если логин корректен - if isUsernameValid { - SecureField(NSLocalizedString("LoginView_password", comment: ""), text: $viewModel.password) + Spacer() + + TextField(NSLocalizedString("LoginView_login", comment: ""), text: $viewModel.username) .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(8) .autocapitalization(.none) - .onChange(of: viewModel.password) { newValue in + .disableAutocorrection(true) + .onChange(of: viewModel.username) { newValue in if newValue.count > 32 { - viewModel.password = String(newValue.prefix(32)) + viewModel.username = String(newValue.prefix(32)) } } - // Показываем ошибку для пароля - if !isPasswordValid && !viewModel.password.isEmpty { - Text(NSLocalizedString("LoginView_error_password_invalid", comment: "Неверный пароль")) + // Показываем ошибку для логина + if !isUsernameValid && !viewModel.username.isEmpty { + Text(NSLocalizedString("LoginView_error_username_invalid", comment: "Неверный логин")) .foregroundColor(.red) .font(.caption) } - } - - if isUsernameValid && isPasswordValid { - Button(action: { - viewModel.login() - }) { - if viewModel.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .padding() - .frame(maxWidth: .infinity) - .background(Color.gray.opacity(0.6)) - .cornerRadius(8) - } else { - Text(NSLocalizedString("LoginView_button_login", comment: "")) - .foregroundColor(.white) - .padding() - .frame(maxWidth: .infinity) - .background(Color.blue) - .cornerRadius(8) + + // Показываем поле пароля (даже если оно невалидное) только если логин корректен + if isUsernameValid || !viewModel.password.isEmpty { + SecureField(NSLocalizedString("LoginView_password", comment: ""), text: $viewModel.password) + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + .autocapitalization(.none) + .onChange(of: viewModel.password) { newValue in + if newValue.count > 32 { + viewModel.password = String(newValue.prefix(32)) + } + } + + // Показываем ошибку для пароля + if !isPasswordValid && !viewModel.password.isEmpty { + Text(NSLocalizedString("LoginView_error_password_invalid", comment: "Неверный пароль")) + .foregroundColor(.red) + .font(.caption) } } - .disabled(viewModel.isLoading) - } + + if isUsernameValid && isPasswordValid { + Button(action: { + viewModel.login() + }) { + if viewModel.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .padding() + .frame(maxWidth: .infinity) + .background(Color.gray.opacity(0.6)) + .cornerRadius(8) + } else { + Text(NSLocalizedString("LoginView_button_login", comment: "")) + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(8) + } + } + .disabled(viewModel.isLoading || !isUsernameValid || !isPasswordValid) + } - Spacer() - - // Кнопка регистрации - Button(action: { - isShowingRegistration = true - }) { - Text(NSLocalizedString("LoginView_button_register", comment: "Регистрация")) - .foregroundColor(.blue) + Spacer() + + // Кнопка регистрации + Button(action: { + isShowingRegistration = true + }) { + Text(NSLocalizedString("LoginView_button_register", comment: "Регистрация")) + .foregroundColor(.blue) + } + .padding(.top, 10) + .sheet(isPresented: $isShowingRegistration) { + RegistrationView(viewModel: viewModel) + } + } - .padding(.top, 10) - .sheet(isPresented: $isShowingRegistration) { - RegistrationView() + .padding() + .alert(isPresented: $viewModel.showError) { + Alert( + title: Text(NSLocalizedString("LoginView_error", comment: "")), + message: Text(viewModel.errorMessage), + dismissButton: .default(Text(NSLocalizedString("ok", comment: ""))) + ) } - + .onTapGesture { + hideKeyboard() } - .padding() - .alert(isPresented: $viewModel.showError) { - Alert( - title: Text(NSLocalizedString("LoginView_error", comment: "")), - message: Text(viewModel.errorMessage), - dismissButton: .default(Text("ОК")) - ) } } + + + + private func toggleTheme() { isDarkMode.toggle() } @@ -141,6 +154,10 @@ struct LoginView: View { guard let url = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(url) } + + private func hideKeyboard() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } } diff --git a/Shared/Views/MainView.swift b/Shared/Views/MainView.swift index d285059..8f742fd 100644 --- a/Shared/Views/MainView.swift +++ b/Shared/Views/MainView.swift @@ -16,21 +16,21 @@ struct MainView: View { ContactsTab() .tabItem { Image(systemName: "person.2.fill") - Text("Контакты") + Text(NSLocalizedString("MainView_contacts", comment: "")) } .tag(0) ChatsTab() .tabItem { Image(systemName: "bubble.left.and.bubble.right.fill") - Text("Чаты") + Text(NSLocalizedString("MainView_chats", comment: "")) } .tag(1) - SettingsTab() + SettingsTab(viewModel: viewModel) .tabItem { Image(systemName: "gearshape.fill") - Text("Настройки") + Text(NSLocalizedString("MainView_settings", comment: "")) } .tag(2) } diff --git a/Shared/Views/RegistrationView.swift b/Shared/Views/RegistrationView.swift index ba60051..fe1aa7e 100644 --- a/Shared/Views/RegistrationView.swift +++ b/Shared/Views/RegistrationView.swift @@ -8,15 +8,19 @@ import SwiftUI struct RegistrationView: View { + @ObservedObject var viewModel: LoginViewModel @Environment(\.presentationMode) private var presentationMode - - @State private var fullName: String = "" + @State private var username: String = "" @State private var password: String = "" @State private var confirmPassword: String = "" @State private var inviteCode: String = "" @AppStorage("isDarkMode") private var isDarkMode: Bool = true + @State private var isLoading: Bool = false + @State private var showError: Bool = false + @State private var errorMessage: String = "" + private var isUsernameValid: Bool { let pattern = "^[A-Za-z0-9_]{3,32}$" return username.range(of: pattern, options: .regularExpression) != nil @@ -31,104 +35,179 @@ struct RegistrationView: View { } private var isFormValid: Bool { - !fullName.isEmpty && isUsernameValid && isPasswordValid && isConfirmPasswordValid + isUsernameValid && isPasswordValid && isConfirmPasswordValid } var body: some View { NavigationView { - VStack { - Text(NSLocalizedString("RegistrationView_title", comment: "Регистрация")) - .font(.largeTitle) - .bold() + ScrollView { + ZStack { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + hideKeyboard() + } -// Spacer() + VStack(alignment: .leading, spacing: 16) { - // Полное имя - TextField(NSLocalizedString("RegistrationView_fullname", comment: "Полное имя"), text: $fullName) - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - .autocapitalization(.words) - .disableAutocorrection(true) + Group { - // Логин - TextField(NSLocalizedString("RegistrationView_login", comment: "Логин"), text: $username) - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - .autocapitalization(.none) - .disableAutocorrection(true) - - if !isUsernameValid && !username.isEmpty { - Text(NSLocalizedString("RegistrationView_error_username_invalid", comment: "Неверный логин")) - .foregroundColor(.red) - .font(.caption) - } - - // Пароль - SecureField(NSLocalizedString("RegistrationView_password", comment: "Пароль"), text: $password) - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - .autocapitalization(.none) - - if !isPasswordValid && !password.isEmpty { - Text(NSLocalizedString("RegistrationView_error_password_invalid", comment: "Пароль должен быть от 6 до 32 символов")) - .foregroundColor(.red) - .font(.caption) - } - - // Подтверждение пароля - SecureField(NSLocalizedString("RegistrationView_confirm_password", comment: "Подтверждение пароля"), text: $confirmPassword) - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - .autocapitalization(.none) - - if !isConfirmPasswordValid && !confirmPassword.isEmpty { - Text(NSLocalizedString("RegistrationView_error_confirm_password_invalid", comment: "Пароли не совпадают")) - .foregroundColor(.red) - .font(.caption) - } - - // Инвайт-код - TextField(NSLocalizedString("RegistrationView_invite", comment: "Инвайт-код"), text: $inviteCode) - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - .autocapitalization(.none) - .disableAutocorrection(true) - - // Кнопка регистрации - Button(action: { - print("Регистрация отправлена") - }) { - Text(NSLocalizedString("RegistrationView_button_register", comment: "Зарегистрироваться")) - .foregroundColor(.white) + HStack { + TextField(NSLocalizedString("RegistrationView_login", comment: "Логин"), text: $username) + .autocapitalization(.none) + .disableAutocorrection(true) + Spacer() + if !username.isEmpty { + Image(systemName: isUsernameValid ? "checkmark.circle" : "xmark.circle") + .foregroundColor(isUsernameValid ? .green : .red) + } + } .padding() - .frame(maxWidth: .infinity) - .background(isFormValid ? Color.blue : Color.gray.opacity(0.6)) + .background(Color(.secondarySystemBackground)) .cornerRadius(8) - } - .disabled(!isFormValid) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: username) { newValue in + if newValue.count > 32 { + username = String(newValue.prefix(32)) + } + } - } - .padding() - .navigationBarItems(trailing: - Button(action: { - presentationMode.wrappedValue.dismiss() - }) { - Text(NSLocalizedString("RegistrationView_close", comment: "Закрыть")) + if !isUsernameValid && !username.isEmpty { + Text(NSLocalizedString("RegistrationView_error_username_invalid", comment: "Неверный логин")) + .foregroundColor(.red) + .font(.caption) + } + + HStack { + SecureField(NSLocalizedString("RegistrationView_password", comment: "Пароль"), text: $password) + .autocapitalization(.none) + Spacer() + if !password.isEmpty { + Image(systemName: isPasswordValid ? "checkmark.circle" : "xmark.circle") + .foregroundColor(isPasswordValid ? .green : .red) + } + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + .autocapitalization(.none) + .onChange(of: password) { newValue in + if newValue.count > 32 { + password = String(newValue.prefix(32)) + } + } + + if !isPasswordValid && !password.isEmpty { + Text(NSLocalizedString("RegistrationView_error_password_invalid", comment: "Пароль должен быть от 6 до 32 символов")) + .foregroundColor(.red) + .font(.caption) + } + + HStack { + SecureField(NSLocalizedString("RegistrationView_confirm_password", comment: "Подтверждение пароля"), text: $confirmPassword) + .autocapitalization(.none) + Spacer() + if !confirmPassword.isEmpty { + Image(systemName: isConfirmPasswordValid ? "checkmark.circle" : "xmark.circle") + .foregroundColor(isConfirmPasswordValid ? .green : .red) + } + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + .autocapitalization(.none) + .onChange(of: confirmPassword) { newValue in + if newValue.count > 32 { + confirmPassword = String(newValue.prefix(32)) + } + } + + if !isConfirmPasswordValid && !confirmPassword.isEmpty { + Text(NSLocalizedString("RegistrationView_error_confirm_password_invalid", comment: "Пароли не совпадают")) + .foregroundColor(.red) + .font(.caption) + } + + TextField(NSLocalizedString("RegistrationView_invite", comment: "Инвайт-код"), text: $inviteCode) + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + .autocapitalization(.none) + .disableAutocorrection(true) + } + + Button(action: registerUser) { + if isLoading { + ProgressView() + .padding() + .frame(maxWidth: .infinity) + .background(Color.gray.opacity(0.6)) + .cornerRadius(8) + } else { + Text(NSLocalizedString("RegistrationView_button_register", comment: "Зарегистрироваться")) + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(isFormValid ? Color.blue : Color.gray.opacity(0.6)) + .cornerRadius(8) + } + } + .disabled(!isFormValid) + .padding(.bottom) } - ) + .padding() + } + .navigationBarItems(trailing: + Button(action: { + presentationMode.wrappedValue.dismiss() + }) { + Text(NSLocalizedString("RegistrationView_close", comment: "Закрыть")) + } + ) + .navigationTitle(Text(NSLocalizedString("RegistrationView_title", comment: "Регистрация"))) + .alert(isPresented: $showError) { + Alert( + title: Text(NSLocalizedString("RegistrationView_error", comment: "Ошибка")), + message: Text(errorMessage), + dismissButton: .default(Text(NSLocalizedString("ok", comment: ""))) + ) + } + } + .onTapGesture { + hideKeyboard() + } + } + .onTapGesture { + hideKeyboard() + } + } + + private func registerUser() { + isLoading = true + errorMessage = "" + viewModel.registerUser(username: username, password: password, invite: inviteCode.isEmpty ? nil : inviteCode) { success, message in + isLoading = false + if success { + presentationMode.wrappedValue.dismiss() + } else { + errorMessage = message ?? "Неизвестная ошибка." + showError = true + } } } + private func hideKeyboard() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } } + struct RegistrationView_Previews: PreviewProvider { static var previews: some View { - RegistrationView() + let viewModel = LoginViewModel() + viewModel.isLoading = false // чтобы убрать спиннер + return RegistrationView(viewModel: viewModel) } } diff --git a/Shared/Views/SettingsTab.swift b/Shared/Views/SettingsTab.swift index e014cc8..777e34f 100644 --- a/Shared/Views/SettingsTab.swift +++ b/Shared/Views/SettingsTab.swift @@ -8,6 +8,8 @@ import SwiftUI struct SettingsTab: View { + @ObservedObject var viewModel: LoginViewModel + var body: some View { NavigationView { VStack { @@ -17,6 +19,15 @@ struct SettingsTab: View { .padding() Spacer() + + Button(action: { + viewModel.logoutCurrentUser() + }) { + Text("Выйти из текущего пользователя") + .foregroundColor(.red) + } + .padding() + } .navigationTitle("Настройки") } diff --git a/Shared/config.swift b/Shared/config.swift index a098dfb..f0c30c8 100644 --- a/Shared/config.swift +++ b/Shared/config.swift @@ -1,9 +1,12 @@ import SwiftUI struct AppConfig { - static var DEBUG: Bool = false + static var DEBUG: Bool = true static let SERVICE = Bundle.main.bundleIdentifier ?? "default.service" static let PROTOCOL = "https" static let API_SERVER = "\(PROTOCOL)://api.volnahub.ru" static let SERVER_TIMEZONE = "GMT+3" + static let USER_AGENT = "volnahub ios" + static let APP_BUILD = "freestore" + static let APP_VERSION = "0.1" } diff --git a/Shared/volnahubApp.swift b/Shared/volnahubApp.swift index 5218d59..fa5ad06 100644 --- a/Shared/volnahubApp.swift +++ b/Shared/volnahubApp.swift @@ -15,7 +15,7 @@ import SwiftUI struct volnahubApp: App { @AppStorage("isDarkMode") private var isDarkMode: Bool = true @StateObject private var viewModel = LoginViewModel() - + var body: some Scene { WindowGroup { ZStack { diff --git a/iOS/Info.plist b/iOS/Info.plist index 8cec9d0..bb81a82 100644 --- a/iOS/Info.plist +++ b/iOS/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS diff --git a/volnahub.xcodeproj/project.pbxproj b/volnahub.xcodeproj/project.pbxproj index 5eefaad..817ffd8 100644 --- a/volnahub.xcodeproj/project.pbxproj +++ b/volnahub.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 1A0276032DF909F900D8BC53 /* refreshtokenex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0276022DF909F900D8BC53 /* refreshtokenex.swift */; }; + 1A0276112DF9247000D8BC53 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0276102DF9247000D8BC53 /* CustomTextField.swift */; }; 1A79408D2DF77BC3002569DA /* volnahubApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79407A2DF77BC2002569DA /* volnahubApp.swift */; }; 1A79408F2DF77BC3002569DA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79407B2DF77BC2002569DA /* ContentView.swift */; }; 1A7940902DF77BC3002569DA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79407B2DF77BC2002569DA /* ContentView.swift */; }; @@ -28,6 +30,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1A0276022DF909F900D8BC53 /* refreshtokenex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = refreshtokenex.swift; sourceTree = ""; }; + 1A0276102DF9247000D8BC53 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = ""; }; 1A79407A2DF77BC2002569DA /* volnahubApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = volnahubApp.swift; sourceTree = ""; }; 1A79407B2DF77BC2002569DA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 1A79407C2DF77BC3002569DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -133,6 +137,7 @@ 1A7940CD2DF7A9AA002569DA /* SettingsTab.swift */, 1A7940E62DF7B5E5002569DA /* SplashScreenView.swift */, 1A79410B2DF7C81D002569DA /* RegistrationView.swift */, + 1A0276102DF9247000D8BC53 /* CustomTextField.swift */, ); path = Views; sourceTree = ""; @@ -157,6 +162,7 @@ isa = PBXGroup; children = ( 1A7940A12DF77DE9002569DA /* AuthService.swift */, + 1A0276022DF909F900D8BC53 /* refreshtokenex.swift */, ); path = Network; sourceTree = ""; @@ -280,8 +286,10 @@ 1A7940E72DF7B5E5002569DA /* SplashScreenView.swift in Sources */, 1A7940B02DF77E26002569DA /* LoginView.swift in Sources */, 1A79410C2DF7C81D002569DA /* RegistrationView.swift in Sources */, + 1A0276112DF9247000D8BC53 /* CustomTextField.swift in Sources */, 1A7940CE2DF7A9AA002569DA /* SettingsTab.swift in Sources */, 1A7940DE2DF7B0D7002569DA /* config.swift in Sources */, + 1A0276032DF909F900D8BC53 /* refreshtokenex.swift in Sources */, 1A7940B62DF77F21002569DA /* MainView.swift in Sources */, 1A7940E22DF7B1C5002569DA /* KeychainService.swift in Sources */, 1A7940A62DF77DF5002569DA /* User.swift in Sources */, @@ -434,8 +442,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1337; + DEVELOPMENT_TEAM = V22H44W47J; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -443,8 +453,10 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = ckp.volnahub; + MARKETING_VERSION = 0.1; + PRODUCT_BUNDLE_IDENTIFIER = ckp2.volnahub; PRODUCT_NAME = volnahub; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -456,8 +468,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1337; + DEVELOPMENT_TEAM = V22H44W47J; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -465,8 +479,10 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = ckp.volnahub; + MARKETING_VERSION = 0.1; + PRODUCT_BUNDLE_IDENTIFIER = ckp2.volnahub; PRODUCT_NAME = volnahub; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/volnahub.xcodeproj/xcuserdata/cheykrym.xcuserdatad/xcschemes/xcschememanagement.plist b/volnahub.xcodeproj/xcuserdata/cheykrym.xcuserdatad/xcschemes/xcschememanagement.plist index 6cdd854..32667f1 100644 --- a/volnahub.xcodeproj/xcuserdata/cheykrym.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/volnahub.xcodeproj/xcuserdata/cheykrym.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ volnahub (iOS).xcscheme_^#shared#^_ orderHint - 1 + 0 volnahub (macOS).xcscheme_^#shared#^_ orderHint - 0 + 1