diff --git a/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSet.swift b/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSet.swift index dc5a6ba5f..62b040091 100644 --- a/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSet.swift +++ b/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSet.swift @@ -19,16 +19,10 @@ import SwiftUI import UIKit -public protocol BPKFieldSetContentView: View { - associatedtype ContentView: BPKFieldSetContentView - - func inputState(_ state: BPKFieldSet.State) -> ContentView -} - // swiftlint:disable line_length /// A component which wraps its content (view) and optionally adds a title, description and error label (depending on the field's state) around it. -/// Supported states are Default, and Error. The states are dispatched to the wrapped view. The wrapped view must conform to `BPKFieldSetStatusHandling` to ensure it can handle the dispatched state. +/// Supported states are Default, and Error. The states are dispatched to the wrapped view through the .environment modifier. /// /// Use `inputState(_ state: State)` to change the state of the field set. /// @@ -37,8 +31,8 @@ public protocol BPKFieldSetContentView: View { // swiftlint:enable line_length -public struct BPKFieldSet: View { - private var state: BPKFieldSet.State = .default +public struct BPKFieldSet: View { + private var state: BPKFieldSetState = .default private let label: String? private let content: Content private let description: String? @@ -47,7 +41,7 @@ public struct BPKFieldSet: View { public init( label: String? = nil, description: String? = nil, - content: () -> Content + @ViewBuilder content: () -> Content ) { self.label = label self.description = description @@ -58,9 +52,9 @@ public struct BPKFieldSet: View { VStack(alignment: .leading, spacing: .sm) { labelView content - .inputState(state) .padding(.bottom, .sm) - .accessibilityIdentifier(accessibilityIdentifier(for: "wrapped_view")) + .environment(\.bpkFieldSetState, state) + .accessibilityIdentifier(accessibilityIdentifier(for: AccessibilityID.content)) descriptionView if case let .error(message) = state { errorMessage(message) @@ -75,7 +69,7 @@ public struct BPKFieldSet: View { BPKText(label, style: .label2) .lineLimit(nil) .foregroundColor(state.labelColor) - .accessibilityIdentifier(accessibilityIdentifier(for: "label")) + .accessibilityIdentifier(accessibilityIdentifier(for: AccessibilityID.label)) } } @@ -85,7 +79,7 @@ public struct BPKFieldSet: View { BPKText(description, style: .caption) .lineLimit(nil) .foregroundColor(state.descriptionColor) - .accessibilityIdentifier(accessibilityIdentifier(for: "descritpion")) + .accessibilityIdentifier(accessibilityIdentifier(for: AccessibilityID.description)) } } @@ -97,11 +91,11 @@ public struct BPKFieldSet: View { BPKText(message, style: .caption) .lineLimit(nil) .foregroundColor(.textErrorColor) - .accessibilityIdentifier(accessibilityIdentifier(for: "error_message")) + .accessibilityIdentifier(accessibilityIdentifier(for: AccessibilityID.errorMessage)) } } - public func inputState(_ state: BPKFieldSet.State) -> BPKFieldSet { + public func inputState(_ state: BPKFieldSetState) -> BPKFieldSet { var result = self result.state = state return result @@ -117,12 +111,30 @@ extension BPKFieldSet { return result } - private func accessibilityIdentifier(for label: String) -> String { + private func accessibilityIdentifier(for subview: AccessibilityID) -> String { if let prefix = accessibilityPrefix { - return "\(prefix)_\(label)" + return "\(prefix)_\(subview.rawValue)" } return "" } + + private enum AccessibilityID: String { + case label + case content = "content_view" + case description + case errorMessage = "error_message" + } +} + +struct BPKFieldSetStateKey: EnvironmentKey { + static var defaultValue: BPKFieldSetState? +} + +extension EnvironmentValues { + var bpkFieldSetState: BPKFieldSetState? { + get { self[BPKFieldSetStateKey.self] } // swiftlint:disable:this implicit_getter + set { self[BPKFieldSetStateKey.self] = newValue } + } } // MARK: - Previews @@ -170,7 +182,7 @@ extension BPKFieldSet { func constructFieldSet( withLabel label: String? = nil, andDescription description: String? = nil, - wrappedView: some BPKFieldSetContentView + wrappedView: some View ) -> some View { ForEach([0, 1], id: \.self) { index in BPKFieldSet(label: label, description: description) { diff --git a/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSetState.swift b/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSetState.swift index e56b6065b..62775bada 100644 --- a/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSetState.swift +++ b/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSetState.swift @@ -18,27 +18,25 @@ import SwiftUI -extension BPKFieldSet { - /// The state of the field set. - public enum State { - /// Default state. - case `default` - /// Error state - /// Adds an errors message beneath the description or the wrapped view if no description is provided. - case error(message: String) - - var labelColor: BPKColor { - switch self { - case .default: return .textPrimaryColor - case .error: return .textErrorColor - } +/// The state of the field set. +public enum BPKFieldSetState: Equatable { + /// Default state. + case `default` + /// Error state + /// Adds an errors message beneath the description or the wrapped view if no description is provided. + case error(message: String) + + var labelColor: BPKColor { + switch self { + case .default: return .textPrimaryColor + case .error: return .textErrorColor } - - var descriptionColor: BPKColor { - switch self { - case .default: return .textSecondaryColor - case .error: return .textSecondaryColor - } + } + + var descriptionColor: BPKColor { + switch self { + case .default: return .textSecondaryColor + case .error: return .textSecondaryColor } } } diff --git a/Backpack-SwiftUI/FieldSet/README.md b/Backpack-SwiftUI/FieldSet/README.md index 14c1c52c9..24e2ab6d7 100644 --- a/Backpack-SwiftUI/FieldSet/README.md +++ b/Backpack-SwiftUI/FieldSet/README.md @@ -20,7 +20,7 @@ FieldSet is a component which wraps its content (view) and optionally adds a title, description and error label (depending on the field's state) around it. -Supported states are Default, and Error. The states are dispatched to the wrapped view. The wrapped view must conform to `BPKFieldSetContentView` to ensure it can handle the dispatched state. +Supported states are Default, and Error. The states are dispatched to the wrapped view through the .environment modifier. ## BPKFieldSet diff --git a/Backpack-SwiftUI/Select/Classes/BPKSelect+FieldSet.swift b/Backpack-SwiftUI/Select/Classes/BPKSelect+FieldSet.swift deleted file mode 100644 index 89ed95390..000000000 --- a/Backpack-SwiftUI/Select/Classes/BPKSelect+FieldSet.swift +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Backpack - Skyscanner's Design System - * - * Copyright © 2023 Skyscanner Ltd. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import SwiftUI - -extension BPKSelect: BPKFieldSetContentView { - public func inputState(_ state: BPKFieldSet.State) -> BPKSelect { - switch state { - case .default: - return inputState(BPKSelect.State.default) - case .error: - return inputState(BPKSelect.State.error) - } - } -} - -#Preview { - BPKFieldSet(label: "Label", description: "Description") { - BPKSelect( - placeholder: "Breakfast Choices", - options: ["Porridge", "Eggs", "Swift UI"], - selectedIndex: .constant(1) - ) - } - .inputState(.error(message: "Error Message")) - .padding() -} diff --git a/Backpack-SwiftUI/Select/Classes/BPKSelect.swift b/Backpack-SwiftUI/Select/Classes/BPKSelect.swift index cee947f3d..b1d3f7513 100644 --- a/Backpack-SwiftUI/Select/Classes/BPKSelect.swift +++ b/Backpack-SwiftUI/Select/Classes/BPKSelect.swift @@ -51,11 +51,22 @@ extension View { } public struct BPKSelect: View { + @Environment(\.bpkFieldSetState) var fieldSetState @Binding private var selectedIndex: Int? private let options: [String] private let placeholder: String private var state: State = .default + private var resolvedState: State { + switch fieldSetState { + case .default: + return .default + case .error: + return .error + default: + return state + } + } var labelText: String { // If nil or invalid value is passed in by consumer we display the placeholder @@ -85,7 +96,7 @@ public struct BPKSelect: View { .bpkPickerStyle( CustomPickerStyle( labelText: labelText, - pickerState: state + pickerState: resolvedState ) ) } diff --git a/Backpack-SwiftUI/TextArea/Classes/BPKTextArea+FieldSet.swift b/Backpack-SwiftUI/TextArea/Classes/BPKTextArea+FieldSet.swift deleted file mode 100644 index 314c94999..000000000 --- a/Backpack-SwiftUI/TextArea/Classes/BPKTextArea+FieldSet.swift +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Backpack - Skyscanner's Design System - * - * Copyright © 2023 Skyscanner Ltd. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import SwiftUI - -extension BPKTextArea: BPKFieldSetContentView { - public func inputState(_ state: BPKFieldSet.State) -> BPKTextArea { - switch state { - case .default: - return inputState(BPKTextArea.State.default) - case .error: - return inputState(BPKTextArea.State.error) - } - } -} - -#Preview { - BPKFieldSet(label: "Label", description: "Description") { - BPKTextArea(.constant(""), placeholder: "Enter text") - } - .inputState(.error(message: "Error Message")) - .frame(height: 150) - .padding() -} diff --git a/Backpack-SwiftUI/TextArea/Classes/BPKTextArea.swift b/Backpack-SwiftUI/TextArea/Classes/BPKTextArea.swift index 11a347589..442917491 100644 --- a/Backpack-SwiftUI/TextArea/Classes/BPKTextArea.swift +++ b/Backpack-SwiftUI/TextArea/Classes/BPKTextArea.swift @@ -37,9 +37,21 @@ public struct BPKTextArea: View { } @Environment(\.colorScheme) var colorScheme + @Environment(\.bpkFieldSetState) var fieldSetState @Binding private var value: String private let placeholder: String? private var state: State = .default + private var resolvedState: State { + switch fieldSetState { + case .default: + return .default + case .error: + return .error + default: + return state + } + } + private var accessibilityLabelText: String { if let placeholder = placeholder, value.isEmpty { return placeholder @@ -113,14 +125,14 @@ public struct BPKTextArea: View { .clipShape( RoundedRectangle(cornerRadius: BorderConstants.cornerRadius) ) - .outline(state.borderColor, cornerRadius: BorderConstants.cornerRadius) + .outline(resolvedState.borderColor, cornerRadius: BorderConstants.cornerRadius) .frame(minHeight: frameHeight) .accessibilityLabel(accessibilityLabelText) } @ViewBuilder private var accessory: some View { - if let icon = state.icon { + if let icon = resolvedState.icon { BPKIconView(icon.icon) .foregroundColor(icon.color) .accessibilityHidden(true) diff --git a/Backpack-SwiftUI/TextField/Classes/BPKTextField+FieldSet.swift b/Backpack-SwiftUI/TextField/Classes/BPKTextField+FieldSet.swift deleted file mode 100644 index fb2189a63..000000000 --- a/Backpack-SwiftUI/TextField/Classes/BPKTextField+FieldSet.swift +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Backpack - Skyscanner's Design System - * - * Copyright © 2023 Skyscanner Ltd. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import SwiftUI - -extension BPKTextField: BPKFieldSetContentView { - public func inputState(_ state: BPKFieldSet.State) -> BPKTextField { - switch state { - case .default: - return inputState(BPKTextField.State.default) - case .error: - return inputState(BPKTextField.State.error) - } - } -} - -#Preview { - BPKFieldSet(label: "Label", description: "Description") { - BPKTextField(placeholder: "Enter text", .constant("")) - } - .inputState(.error(message: "Error Message")) - .padding() -} diff --git a/Backpack-SwiftUI/TextField/Classes/BPKTextField.swift b/Backpack-SwiftUI/TextField/Classes/BPKTextField.swift index 46d126f7a..069a84aa0 100644 --- a/Backpack-SwiftUI/TextField/Classes/BPKTextField.swift +++ b/Backpack-SwiftUI/TextField/Classes/BPKTextField.swift @@ -26,11 +26,21 @@ public struct BPKTextField: View { let icon: BPKIcon let color: BPKColor } + @Environment(\.bpkFieldSetState) var fieldSetState @Binding private var text: String private let placeholder: String private var state: State = .default - + private var resolvedState: State { + switch fieldSetState { + case .default: + return .default + case .error: + return .error + default: + return state + } + } /// Creates a `BPKTextField`. /// /// - Parameters: @@ -48,14 +58,14 @@ public struct BPKTextField: View { HStack { TextField(placeholder, text: $text) .font(style: .bodyDefault) - .foregroundColor(state.textColor) - .disabled(state.isDisabled) + .foregroundColor(resolvedState.textColor) + .disabled(resolvedState.isDisabled) accessory } .padding(.md) .background(.surfaceDefaultColor) .clipShape(RoundedRectangle(cornerRadius: .sm)) - .outline(state.borderColor, cornerRadius: .sm) + .outline(resolvedState.borderColor, cornerRadius: .sm) .if(!BPKFont.enableDynamicType, transform: { $0.sizeCategory(.large) }) @@ -63,8 +73,8 @@ public struct BPKTextField: View { private var accessory: some View { HStack { - if let icon = state.icon { - if case let .clear(accessibilityLabel, action) = state { + if let icon = resolvedState.icon { + if case let .clear(accessibilityLabel, action) = resolvedState { Button(action: action) { BPKIconView(icon.icon) .foregroundColor(icon.color) diff --git a/Example/Backpack/SwiftUI/Components/FieldSet/FieldSetExampleView.swift b/Example/Backpack/SwiftUI/Components/FieldSet/FieldSetExampleView.swift index 92d265c02..eed4e7a80 100644 --- a/Example/Backpack/SwiftUI/Components/FieldSet/FieldSetExampleView.swift +++ b/Example/Backpack/SwiftUI/Components/FieldSet/FieldSetExampleView.swift @@ -32,19 +32,19 @@ struct FieldSetExampleView: View { constructFieldSet( withLabel: "Label", andDescription: "Description", - wrappedView: BPKTextField(placeholder: "Enter text", .constant("")) + wrappedView: BPKTextField(placeholder: "Enter text", $text1) ) Divider() BPKText("With Label & No Description", style: .label1) constructFieldSet( withLabel: "Label", - wrappedView: BPKTextField(placeholder: "Enter text", .constant("")) + wrappedView: BPKTextField(placeholder: "Enter text", $text1) ) Divider() BPKText("With No Label & Description", style: .label1) constructFieldSet( andDescription: "Description", - wrappedView: BPKTextArea(.constant(""), placeholder: "Enter text") + wrappedView: BPKTextArea($text1, placeholder: "Enter text") ) Divider() BPKText("With No Label & No Description", style: .label1) @@ -64,7 +64,7 @@ struct FieldSetExampleView: View { private func constructFieldSet( withLabel label: String? = nil, andDescription description: String? = nil, - wrappedView: some BPKFieldSetContentView + wrappedView: some View ) -> some View { BPKFieldSet(label: label, description: description) { wrappedView