190 lines
6.6 KiB
Swift
190 lines
6.6 KiB
Swift
//
|
||
// SQRadio.swift
|
||
// Sequoia
|
||
//
|
||
// Created by Victor on 10/10/2024.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
enum SQRadioOrientation {
|
||
case horizontal
|
||
case vertical
|
||
}
|
||
|
||
struct SQRadioOption: Identifiable {
|
||
let id: Int = UUID().hashValue
|
||
let title: String
|
||
var desc: String? = nil
|
||
|
||
init(title: String, desc: String? = nil) {
|
||
self.title = title
|
||
self.desc = desc
|
||
}
|
||
}
|
||
|
||
struct SQRadio: View {
|
||
let title: String?
|
||
let titleSize: CGFloat
|
||
var radioTextSize: CGFloat = 16
|
||
var orientation: SQRadioOrientation = .vertical
|
||
let options: [SQRadioOption]
|
||
var error: Binding<SQFormFieldError>
|
||
@Binding var selectedIndex: Int?
|
||
|
||
init(title: String? = nil, titleSize: CGFloat = 24, radioTextSize: CGFloat = 16, orientation: SQRadioOrientation = .vertical, options: [SQRadioOption], selectedIndex: Binding<Int?>, error: Binding<SQFormFieldError> = .constant(.none)) {
|
||
self.title = title
|
||
self.titleSize = titleSize
|
||
self.radioTextSize = radioTextSize
|
||
self.orientation = orientation
|
||
self.options = options
|
||
self.error = error
|
||
self._selectedIndex = selectedIndex
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(alignment: orientation == .vertical ? .leading : .center, spacing: 16) {
|
||
if let title = title {
|
||
SQText(title, size: titleSize, font: .bold)
|
||
.multilineTextAlignment(.center)
|
||
}
|
||
Group {
|
||
if orientation == .horizontal {
|
||
HorizontalRadioButtons(options: options, titleSize: radioTextSize, selectedIndex: $selectedIndex, isInError: error.wrappedValue.isInError)
|
||
.frame(maxWidth: .infinity)
|
||
|
||
} else {
|
||
VStack(alignment: .leading, spacing: 16) {
|
||
radioButtons
|
||
}
|
||
}
|
||
}
|
||
|
||
if error.wrappedValue != .none {
|
||
SQText(error.wrappedValue.message, size: 12, font: .demiBold, textColor: Color.sqSemanticRed)
|
||
}
|
||
}
|
||
}
|
||
|
||
private var radioButtons: some View {
|
||
ForEach(Array(options.enumerated()), id: \.offset) { index, option in
|
||
RadioButton(
|
||
title: option.title,
|
||
desc: option.desc,
|
||
titleSize: radioTextSize,
|
||
isSelected: Binding(
|
||
get: { selectedIndex == index },
|
||
set: { _ in selectedIndex = index }
|
||
),
|
||
isInError: error.wrappedValue.isInError,
|
||
orientation: orientation
|
||
)
|
||
}
|
||
}
|
||
|
||
private struct HorizontalRadioButtons: View {
|
||
let options: [SQRadioOption]
|
||
let titleSize: CGFloat
|
||
@Binding var selectedIndex: Int?
|
||
var isInError: Bool
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
HStack(alignment: .top, spacing: 0) {
|
||
Spacer()
|
||
ForEach(Array(options.enumerated()), id: \.offset) { index, option in
|
||
RadioButton(
|
||
title: option.title,
|
||
desc: option.desc,
|
||
titleSize: titleSize,
|
||
isSelected: Binding(
|
||
get: { selectedIndex == index },
|
||
set: { _ in selectedIndex = index }
|
||
),
|
||
isInError: isInError,
|
||
orientation: .horizontal
|
||
)
|
||
.frame(width: geometry.size.width / CGFloat(options.count))
|
||
}
|
||
Spacer()
|
||
}
|
||
}
|
||
.frame(height: 80)
|
||
}
|
||
}
|
||
|
||
private struct RadioButton: View {
|
||
let title: String
|
||
let desc: String?
|
||
let titleSize: CGFloat
|
||
@Binding var isSelected: Bool
|
||
var isInError: Bool
|
||
let orientation: SQRadioOrientation
|
||
|
||
var body: some View {
|
||
Group {
|
||
if orientation == .horizontal {
|
||
VStack(spacing: 8) {
|
||
radioCircle
|
||
SQText(title)
|
||
.multilineTextAlignment(.center)
|
||
}
|
||
} else {
|
||
VStack(alignment: .leading) {
|
||
HStack(spacing: 8) {
|
||
radioCircle
|
||
SQText(title)
|
||
}
|
||
|
||
if let desc = desc {
|
||
SQText(desc, size: 12)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.contentShape(Rectangle()) // Make the entire area tappable
|
||
.onTapGesture {
|
||
isSelected.toggle()
|
||
}
|
||
}
|
||
|
||
private var radioCircle: some View {
|
||
ZStack {
|
||
Circle()
|
||
.stroke(isInError ? Color.sqSemanticRed :
|
||
(isSelected ?
|
||
Color.sqNeutral(100) :
|
||
Color.sqNeutral(30)), lineWidth: 1)
|
||
.frame(width: 20, height: 20)
|
||
|
||
if isSelected {
|
||
Circle()
|
||
.inset(by: 3)
|
||
.stroke(isInError ? Color.sqSemanticRed : Color.sqNeutral(100), lineWidth: 6)
|
||
.frame(width: 20, height: 20)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
VStack(alignment: .leading, spacing: 32) {
|
||
SQRadio(title: "Vertical", options: [
|
||
SQRadioOption(title: "Option 1", desc: "L’utilisateur semble proposer un service relevant d’une activité réglementée sans disposer des autorisations requises."),
|
||
SQRadioOption(title: "Option 2", desc: "L’utilisateur semble évoquer ou proposer des activités interdites par la loi (trafic d’animaux, substances illicites, armes, etc.)."),
|
||
SQRadioOption(title: "Option 3", desc: "L’utilisateur semble fournir de fausses informations dans son profil."),
|
||
SQRadioOption(title: "Option 4", desc: "L’utilisateur semble avoir usurpé mon identité ou l’identité d’un autre utilisateur (SIRET, numéro de téléphone...).")
|
||
], selectedIndex: .constant(0), error: .constant(.custom("Merci de détailler votre signalement")))
|
||
|
||
SQRadio(title: "Horizontal", orientation: .horizontal, options: [
|
||
SQRadioOption(title: "1\n Non Jamais"),
|
||
SQRadioOption(title: "2"),
|
||
SQRadioOption(title: "3"),
|
||
SQRadioOption(title: "4"),
|
||
SQRadioOption(title: "5\nOui")
|
||
], selectedIndex: .constant(2))
|
||
}
|
||
.padding()
|
||
}
|