Skip to content

Server Errors

swift
import SwiftUI
import FormCraft

private struct ServerError: LocalizedError {
    let code: Int
    let errors: [String: [String]]

    var errorDescription: String? { "Server error \(code)" }
}

private struct PlayerRepository {
    struct PlayerDTO {
        let firstName: String
        let lastName: String
    }

    func fetchPlayer() async -> PlayerDTO {
        try? await Task.sleep(nanoseconds: 3_000_000_000)
        return .init(firstName: "First name", lastName: "Last name")
    }

    func updatePlayer(player: PlayerDTO) async -> Result<PlayerDTO, ServerError> {
        try? await Task.sleep(nanoseconds: 3_000_000_000)

        if player.firstName.count > 4 {
            return .failure(
                .init(
                    code: 400,
                    errors: ["firstName": ["First name must be 4 characters or fewer."]]
                )
            )
        }

        return .success(player)
    }
}

@FormCraft
private struct FormFields: FormCraftFields {
    var firstName = FormCraftField(value: "") { value in
        await FormCraftValidationRules()
            .string()
            .notEmpty()
            .validate(value: value)
    }

    var lastName = FormCraftField(value: "") { value in
        await FormCraftValidationRules()
            .string()
            .notEmpty()
            .validate(value: value)
    }
}

struct ServerErrorsFormView: View {
    @State private var form = FormCraft(fields: FormFields())
    private let playerService = PlayerRepository()

    private func handleSubmit(data: FormCraftValidatedFields<FormFields>) async {
        let response = await playerService.updatePlayer(
            player: .init(
                firstName: data.firstName,
                lastName: data.lastName
            )
        )

        switch response {
        case .success:
            print("Profile updated.")
        case .failure(let serverError):
            form.setErrors(
                errors: serverError.errors,
                options: .init(shouldFocusError: false)
            )
        }
    }

    func onTask() async {
        let data = await playerService.fetchPlayer()
        form.setDefaultValues(
            (\.firstName, data.firstName),
            (\.lastName, data.lastName)
        )
    }

    var body: some View {
        VStack {
            FormCraftView(formConfig: form) {
                FormCraftControllerView(
                    formConfig: form,
                    key: \.firstName
                ) { value, field in
                    TextField("First name", text: value)
                        .textFieldStyle(.roundedBorder)

                    if let firstError = field.errors?.messages.first {
                        Text(firstError)
                            .foregroundStyle(.red)
                    }
                }

                FormCraftControllerView(
                    formConfig: form,
                    key: \.lastName
                ) { value, field in
                    TextField("Last name", text: value)
                        .textFieldStyle(.roundedBorder)

                    if let firstError = field.errors?.messages.first {
                        Text(firstError)
                            .foregroundStyle(.red)
                    }
                }
            }

            Button("Save", action: form.handleSubmit(onSuccess: handleSubmit))
                .disabled(form.formState.isSubmitting)
        }
        .task { await onTask() }
    }
}

Released under the MIT License.