Untitled
unknown
plain_text
6 days ago
9.8 kB
15
Indexable
import SwiftUI import Foundation import LifenessCore import LifenessUI import LifenessCommon struct EditPersonalDataView: View { @Environment(\.window) var window @EnvironmentObject var store: Store<AppState> @StateObject var viewModel: EditPersonalDataViewModel // UI State variables @State private var showValidationErrors: Bool = false @State private var showAlert: Bool = false @State private var isDobValid: Bool = true @State private var dobError: String? = nil let state: NavigationItem.EditPersonalData private let initialData: PersonalData @State private var isUsernameInitiallyEmpty: Bool init(state: NavigationItem.EditPersonalData) { self.state = state self._viewModel = .init(wrappedValue: .init(personalData: state.data)) self.initialData = state.data self._isUsernameInitiallyEmpty = State(initialValue: state.data.username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } var body: some View { ZStack { Color.custom(.grey200).edgesIgnoringSafeArea(.all) let isDiga = store.context.env.appVariant == .diga let isDefaultVariant = store.context.env.appVariant == .default VStack(spacing: 0) { ScrollView { VStack(spacing: 16) { SectionHeaderView(localized("profile_text")) validatedInput($viewModel.personalInfo.firstName, forField: "firstName") validatedInput($viewModel.personalInfo.lastName, forField: "lastName") validatedInput($viewModel.personalInfo.username, forField: "username") .disabled(!isUsernameInitiallyEmpty) .submitLabel(.done) .onSubmit { window?.endEditing(true) } let isWellapyfr = store.context.env.appVariant == .wellapyfr PhoneInputView(viewModel: $viewModel.personalInfo.phone) .keyboardType(.numberPad) .disabled(!isWellapyfr) if !isDefaultVariant { validatedInput($viewModel.personalInfo.email, forField: "email") .keyboardType(.emailAddress) .disabled(isWellapyfr == false) } if isDiga { InputView(viewModel: $viewModel.personalInfo.bornYear) } else { let dobBinding = Binding<Date?>( get: { let value = viewModel.personalInfo.dob return Calendar.current.isDate(value ?? Date(), inSameDayAs: Date()) ? nil : value }, set: { viewModel.personalInfo.dob = $0 } ) VStack(alignment: .leading, spacing: 2) { DatePickerNullable( placeholder: DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .none), selection: dobBinding, dateUpperBound: Date(), isClearable: false, onDone: { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { validateDate(showAlertOnFailure: true) } } ) if let error = dobError { Text(error) .textStyle(.body2(.medium), color: .custom(.red)) .padding(2) .padding(.horizontal, 22) .frame(maxWidth: .infinity, alignment: .leading) } } } GenderPickerCard($viewModel.personalStats.gender) .padding(.bottom, 8) .onChange(of: viewModel.personalStats.gender) { _ in viewModel.updateHasChanges() } SectionHeaderView(localized("measurements_text")) Group { validatedInput($viewModel.personalStats.startingWeight, forField: "startingWeight") validatedInput($viewModel.personalStats.maxWeight, forField: "maxWeight") validatedInput($viewModel.personalStats.minWeight, forField: "minWeight") validatedInput($viewModel.personalStats.currentWeight, forField: "currentWeight") InputView(viewModel: $viewModel.personalStats.currentWaistCircumference) } .keyboardType(.decimalPad) validatedInput($viewModel.personalStats.height, forField: "height") .keyboardType(.numberPad) VStack(alignment: .leading) { CheckBoxButton( isChecked: $viewModel.dataProcessingAgreement, label: localized(\.common_above_data_correct) ) .textStyle(.body1(.regular)) .padding(.top, 18) .fixedSize(horizontal: false, vertical: true) } } .padding(.horizontal, 24) .padding(.vertical, 26) } BottomButton(title: localized("save_button")) { saveTapped() } .disabled(!(viewModel.hasChanges && viewModel.dataProcessingAgreement)) } } .navigationBarTitle( Text(localized("profile_edit_data")), displayMode: .inline ) .onTapEndEditing() .navigationBarHidden(false) .navigationBarExitButton { store.send(UserAction.ExitPersonalDataEdit( initial: state.data, current: viewModel.createPersonalData() )) } } private func validatedInput(_ field: Binding<InputViewModel>, forField fieldName: String) -> some View { InputView(viewModel: Binding( get: { var updated = field.wrappedValue if showValidationErrors { updated.error = validateField(updated.text, forField: fieldName) } return updated }, set: { field.wrappedValue = $0 } )) } private func validateField(_ value: String, forField field: String) -> String? { let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty && ["firstName", "lastName", "currentWeight", "height"].contains(field) { return localized("input_required_error") } if field == "username" { if trimmed.count < 5 { return localized("input_min_five_characters_required") } let regex = #"^[a-zA-Z][a-zA-Z0-9]{4,24}$"# if !trimmed.matches(regex) { return localized("input_username_invalid_format") } } if field == "email" { if trimmed.isEmpty { return localized("input_required_error") } let regex = #"[^@]+@[^.]+\..+"# if !trimmed.matches(regex) { return localized(\.common_input_error_invalid_email) } } if let number = Double(trimmed), ["currentWeight", "startingWeight", "minWeight", "maxWeight"].contains(field) { if number <= 0 || number >= 500 { return localized("input_value_must_be_less_than_500") } } return nil } private func validateDate(showAlertOnFailure: Bool = false) { guard let date = viewModel.personalInfo.dob else { dobError = localized("input_required_error") isDobValid = false return } let fiveYearsAgo = Calendar.current.date(byAdding: .year, value: -5, to: Date())! if date > fiveYearsAgo { dobError = localized("date_too_recent_error") isDobValid = false if showAlertOnFailure { showAlert = true } } else { isDobValid = true dobError = nil } } private func saveTapped() { window?.endEditing(true) showValidationErrors = true let isWellapyfr = store.context.env.appVariant == .wellapyfr validateDate(showAlertOnFailure: false) var hasErrors = false if isWellapyfr { let phoneError = viewModel.personalInfo.phone.phoneNumber.validatePhoneNumber() viewModel.personalInfo.phone.error = phoneError if phoneError != nil { hasErrors = true } } if hasErrors { return } guard isDobValid else { return } var fieldsToValidate = [ ($viewModel.personalInfo.firstName, "firstName"), ($viewModel.personalInfo.lastName, "lastName"), ($viewModel.personalInfo.username, "username"), ($viewModel.personalStats.currentWeight, "currentWeight"), ($viewModel.personalStats.height, "height") ] if isWellapyfr { fieldsToValidate.append(($viewModel.personalInfo.email, "email")) } for (field, fieldName) in fieldsToValidate { let error = validateField(field.wrappedValue.text, forField: fieldName) field.wrappedValue.error = error } guard fieldsToValidate.allSatisfy({ validateField($0.wrappedValue.text, forField: $1) == nil }) else { return } guard let data = viewModel.createPersonalDataIfValid() else { return } store.send(UserAction.UpdatePersonalData(data)) } } #if DEBUG #Preview { navigationPreview( EditPersonalDataView.init, NavigationItem.EditPersonalData(data: .testData(), id: "1") ) .environmentObject(storePreview(createAppState())) } #endif fileprivate extension String { func matches(_ pattern: String) -> Bool { return self.range(of: pattern, options: .regularExpression) != nil } } extension String { func validatePhoneNumber() -> String? { let trimmed = self.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return localized("input_required_error") } let digits = trimmed.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() if !(8...20).contains(digits.count) { return localized("phone_number_invalid") } return nil } }
Editor is loading...
Leave a Comment