Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking β€œSign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IOS-10813 Make Forms component in SwiftUI #419

Merged
merged 4 commits into from
Dec 16, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions MisticaCatalog/MisticaCatalog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@
18E485A6287F19EB0052A6F2 /* UICatalogHeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E48577287F19EB0052A6F2 /* UICatalogHeaderViewController.swift */; };
244D00C62C491D4600424AA5 /* SkeletonsCatalogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 244D00C52C491D4600424AA5 /* SkeletonsCatalogView.swift */; };
244D00C82C49392700424AA5 /* UICatalogSkeletonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 244D00C72C49392700424AA5 /* UICatalogSkeletonsViewController.swift */; };
24D94F0D2D01D70900CCBEB2 /* FormViewCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D94F0C2D01D70900CCBEB2 /* FormViewCatalog.swift */; };
392E03DC28C6153C0081780B /* UICatalogSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392E03DB28C6153C0081780B /* UICatalogSheetViewController.swift */; };
3968C75E28C9E19600561194 /* UIStepperTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3968C75D28C9E19600561194 /* UIStepperTableViewCell.swift */; };
84038E0A2C38382E003E90F6 /* Telefonica Sans Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 84038E092C38382E003E90F6 /* Telefonica Sans Regular.otf */; };
@@ -142,6 +143,7 @@
18E48577287F19EB0052A6F2 /* UICatalogHeaderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICatalogHeaderViewController.swift; sourceTree = "<group>"; };
244D00C52C491D4600424AA5 /* SkeletonsCatalogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonsCatalogView.swift; sourceTree = "<group>"; };
244D00C72C49392700424AA5 /* UICatalogSkeletonsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICatalogSkeletonsViewController.swift; sourceTree = "<group>"; };
24D94F0C2D01D70900CCBEB2 /* FormViewCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormViewCatalog.swift; sourceTree = "<group>"; };
392E03DB28C6153C0081780B /* UICatalogSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICatalogSheetViewController.swift; sourceTree = "<group>"; };
3968C75D28C9E19600561194 /* UIStepperTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStepperTableViewCell.swift; sourceTree = "<group>"; };
84038E092C38382E003E90F6 /* Telefonica Sans Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Telefonica Sans Regular.otf"; sourceTree = "<group>"; };
@@ -215,6 +217,7 @@
18E4854C287F19EB0052A6F2 /* Components */ = {
isa = PBXGroup;
children = (
24D94F0C2D01D70900CCBEB2 /* FormViewCatalog.swift */,
18E4855A287F19EB0052A6F2 /* BadgeCatalogView.swift */,
18E48556287F19EB0052A6F2 /* ButtonCatalogView.swift */,
18E48551287F19EB0052A6F2 /* CalloutCatalogView.swift */,
@@ -557,6 +560,7 @@
18E48580287F19EB0052A6F2 /* SnackbarCatalogView.swift in Sources */,
18E485A2287F19EB0052A6F2 /* UICatalogFilterViewController.swift in Sources */,
18E48590287F19EB0052A6F2 /* UICatalogFeedbacksViewController.swift in Sources */,
24D94F0D2D01D70900CCBEB2 /* FormViewCatalog.swift in Sources */,
18E3452C289D46C5005E6D81 /* FontsView.swift in Sources */,
18E48597287F19EB0052A6F2 /* UICatalogCardsViewController.swift in Sources */,
18E48596287F19EB0052A6F2 /* UICatalogFormViewController.swift in Sources */,
3 changes: 2 additions & 1 deletion MisticaCatalog/Source/Catalog/CatalogList.swift
Original file line number Diff line number Diff line change
@@ -93,12 +93,13 @@ private extension CatalogRow {
ChipCatalogView()
case .header:
HeaderCatalogView()
case .forms:
FormViewCatalog()
case .tooltip,
.viewStates,
.title,
.filter,
.scrollContentIndicator,
.forms,
.controls,
.sheet:
notImplementedView
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// FormViewCatalog.swift
//
// Made with ❀️ by Novum
//
// Copyright Β© Telefonica. All rights reserved.
//

import MisticaSwiftUI
import SwiftUI

public struct FormViewCatalog: View {
struct FormFieldState: Identifiable {
let id = UUID()
let placeholder: String
var text: String = ""
var assistiveText: String = ""
var state: InputField.ValidationState = .normal
let style: InputField.Style

func createInputField(binding: Binding<FormFieldState>) -> InputField {
InputField(
placeholder: placeholder,
text: binding.text,
assistiveText: binding.assistiveText,
state: binding.state,
nonOptionalFieldFailureMessage: "This field is required"
)
.style(style)
}
}

@State private var fields: [FormFieldState] = [
FormFieldState(placeholder: "Name", style: .text),
FormFieldState(placeholder: "Surname (Optional)", style: .text),
FormFieldState(placeholder: "Email", style: .email),
FormFieldState(placeholder: "Password", style: .secure),
FormFieldState(placeholder: "Phone", style: .phone(code: "+34"))
]

public var body: some View {
FormView(
inputFields: fields.indices.map { index in
fields[index].createInputField(binding: $fields[index])
},
headerView: AnyView(
Text("Header View")
.font(.headline)
),
detailView: AnyView(
Text("Detail View")
.font(.subheadline)
),
footerView: AnyView(
Text("Footer View")
.font(.footnote)
),
buttonTitle: "Save",
onButtonTap: { _ in
validateForm()
}
)
}

private func validateForm() {
for index in fields.indices {
fields[index].createInputField(binding: $fields[index]).validate()
}
}
}

struct CatalogFormView_Previews: PreviewProvider {
static var previews: some View {
FormViewCatalog()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// String+Utils.swift
// String+InputField.swift
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well seen πŸ•΅οΈ

//
// Made with ❀️ by Novum
//
105 changes: 105 additions & 0 deletions Sources/MisticaSwiftUI/Components/Form/FormView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//
// FormView.swift
//
// Made with ❀️ by Novum
//
// Copyright Β© Telefonica. All rights reserved.
//

import SwiftUI

public struct FormView: View {
@State public var inputFields: [InputField]
@State private var isButtonEnabled: Bool = true
@State private var isValid: Bool = true

var headerView: AnyView?
var detailView: AnyView?
var footerView: AnyView?
var buttonTitle: String
var onButtonTap: ((Bool) -> Void)?

public init(
inputFields: [InputField],
headerView: AnyView? = nil,
detailView: AnyView? = nil,
footerView: AnyView? = nil,
buttonTitle: String,
onButtonTap: ((Bool) -> Void)? = nil
) {
self.inputFields = inputFields
self.headerView = headerView
self.detailView = detailView
self.footerView = footerView
self.buttonTitle = buttonTitle
self.onButtonTap = onButtonTap
}

public var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
if let headerView = headerView {
headerView
}

ForEach(inputFields) { inputField in
inputField
}

if let detailView = detailView {
detailView
}

Button(action: validateAndSubmit) {
Text(buttonTitle)
.frame(maxWidth: .infinity)
}
.buttonStyle(.misticaPrimary())
.disabled(!isButtonEnabled)

if let footerView = footerView {
footerView
}
}
.padding(16)
}
}

private func validateAndSubmit() {
onButtonTap?(isValid)
}
}

struct FormView_Previews: PreviewProvider {
@State static var text1 = ""
@State static var text2 = ""
@State static var text3 = ""
@State static var text4 = ""

@State static var assistiveText1 = "This field is required"

static var previews: some View {
FormView(
inputFields: [
InputField(placeholder: "Name", text: $text1)
.style(.text),
InputField(placeholder: "Surname", text: $text2, assistiveText: $assistiveText1, state: .constant(.invalid))
.style(.text),
InputField(placeholder: "Email", text: $text3)
.style(.email),
InputField(placeholder: "Phone", text: $text4)
.style(.phone(code: "+34"))
],
headerView: AnyView(Text("Header view")
.font(.headline)),
detailView: AnyView(Text("Detail view")
.font(.subheadline)),
footerView: AnyView(Text("Footer view")
.font(.footnote)),
buttonTitle: "Save",
onButtonTap: { isValid in
print("Form Submitted. Is valid: \(isValid)")
}
)
}
}
37 changes: 35 additions & 2 deletions Sources/MisticaSwiftUI/Components/Inputfield/InputField.swift
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
// Copyright Β© Telefonica. All rights reserved.
//

import MisticaCommon
import SwiftUI

private enum Constants {
@@ -16,7 +17,7 @@ private enum Constants {
static let textfieldHeight: CGFloat = 24
}

public struct InputField: View {
public struct InputField: View, Identifiable {
public enum ValidationState: Int, Identifiable, Equatable {
case normal
case invalid
@@ -41,20 +42,26 @@ public struct InputField: View {

@State private var editing = false
@State private var secureActivated = true
public var isOptional = false
public var nonOptionalFieldFailureMessage: String?
public var validationStrategy: InputFieldValidationStrategy?

public let id = UUID()
var style: Style = .text
var placeholder: String

public init(
placeholder: String = "",
text: Binding<String>,
assistiveText: Binding<String> = .constant(""),
state: Binding<ValidationState> = .constant(.normal)
state: Binding<ValidationState> = .constant(.normal),
nonOptionalFieldFailureMessage: String = ""
) {
self.placeholder = placeholder
_text = text
_assistiveText = assistiveText
_state = state
self.nonOptionalFieldFailureMessage = nonOptionalFieldFailureMessage
}

public var body: some View {
@@ -113,6 +120,10 @@ public struct InputField: View {
}
}
.animation(.misticaTimingCurve, value: assistiveText.isEmpty)
.onChange(of: text) { _ in
assistiveText = ""
state = .normal
}
}
}

@@ -289,6 +300,28 @@ public extension InputField {
_ = view.textField.textContentType(textContentType)
return view
}

func validate() {
switch validationResult() {
case .success:
state = .normal
case .failure(let message):
show(errorText: message)
}
}

func validationResult() -> InputFieldValidationResult {
if !isOptional && text.isEmpty {
return InputFieldValidationResult.failure(message: nonOptionalFieldFailureMessage ?? "")
} else {
return validationStrategy?.validate(text: text) ?? .success
}
}

func show(errorText: String) {
state = .invalid
assistiveText = errorText
}
}

// MARK: Previews