Skip to content

Commit

Permalink
Adding text field swiftui component (#1626)
Browse files Browse the repository at this point in the history
  • Loading branch information
frugoman committed Apr 14, 2023
1 parent 858fc12 commit 1944985
Show file tree
Hide file tree
Showing 32 changed files with 414 additions and 31 deletions.
28 changes: 0 additions & 28 deletions Backpack-SwiftUI/Badge/Classes/BPKBadge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,34 +70,6 @@ public struct BPKBadge: View {
}
}

// Helper functions to apply the outline style
fileprivate struct Outline: ViewModifier {
let color: BPKColor
let cornerRadius: BPKCornerRadius

func body(content: Content) -> some View {
return content
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(Color(color))
)
}
}

fileprivate extension View {
@ViewBuilder
func outline(_ color: BPKColor?, cornerRadius: BPKCornerRadius) -> some View {
if let color = color {
ModifiedContent(
content: self,
modifier: Outline(color: color, cornerRadius: cornerRadius)
)
} else {
self
}
}
}

struct BPKBadge_Previews: PreviewProvider {
static var previews: some View {
let styles: [(BPKBadge.Style, String, BPKIcon, Bool)] = [
Expand Down
57 changes: 57 additions & 0 deletions Backpack-SwiftUI/Tests/TextField/BPKTextFieldTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018 Skyscanner Ltd
*
* 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 XCTest
import SwiftUI
@testable import Backpack_SwiftUI

class BPKTextFieldTests: XCTestCase {
func test_defaultSettings() {
assertSnapshot(BPKTextField(.constant("Value"))
.frame(width: 300))
}

func test_emptyField() {
assertSnapshot(BPKTextField(.constant(""))
.frame(width: 300))
}

func test_disabledField() {
assertSnapshot(BPKTextField(.constant("Value"))
.inputState(.disabled)
.frame(width: 300))
}

func test_clearableField() {
assertSnapshot(BPKTextField(.constant("Value"))
.inputState(.clear(accessibilityLabel: "Clear") { })
.frame(width: 300))
}

func test_validField() {
assertSnapshot(BPKTextField(.constant("Value"))
.inputState(.valid)
.frame(width: 300))
}

func test_errorField() {
assertSnapshot(BPKTextField(.constant("Value"))
.inputState(.error)
.frame(width: 300))
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
110 changes: 110 additions & 0 deletions Backpack-SwiftUI/TextField/Classes/BPKTextField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018-2022 Skyscanner Ltd
*
* 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

/// A control that displays an editable text interface.
///
/// Use `inputState(_ state: State)` to change the state of the text field.
public struct BPKTextField: View {
struct Icon {
let icon: BPKIcon
let color: BPKColor
}

@Binding private var text: String
private let placeholder: String
private var state: State = .default

/// Creates a `BPKTextField`.
///
/// - Parameters:
/// - placeholder: The placeholder text to display when the text field is empty.
/// - text: The text to display in the text field.
public init(
placeholder: String = "",
_ text: Binding<String>
) {
self.placeholder = placeholder
self._text = text
}

public var body: some View {
HStack {
TextField(placeholder, text: $text)
.font(style: .bodyDefault)
.foregroundColor(state.textColor)
.disabled(state.isDisabled)
accessory
}
.padding(.md)
.background(.surfaceDefaultColor)
.clipShape(RoundedRectangle(cornerRadius: .sm))
.outline(state.borderColor, cornerRadius: .sm)
}

private var accessory: some View {
HStack {
if let icon = state.icon {
if case let .clear(accessibilityLabel, action) = state {
Button(action: action) {
BPKIconView(icon.icon)
.foregroundColor(icon.color)
}
.accessibilityLabel(accessibilityLabel)
} else {
BPKIconView(icon.icon)
.foregroundColor(icon.color)
.accessibilityHidden(true)
}
}
}
}

public func inputState(_ state: State) -> BPKTextField {
var result = self
result.state = state
return result
}
}

fileprivate extension TextField {
func font(style: BPKFontStyle) -> some View {
self.font(Font(style.font))
}
}

struct BPKTextField_Previews: PreviewProvider {
static var previews: some View {
VStack {
BPKTextField(.constant(""))
BPKTextField(placeholder: "Enter", .constant(""))
BPKTextField(.constant("Value"))
BPKTextField(.constant("Disabled"))
.inputState(.disabled)
BPKTextField(.constant("Value"))
.inputState(.error)
BPKTextField(.constant("Value"))
.inputState(.clear(accessibilityLabel: "clear", action: {}))
BPKTextField(.constant("Value"))
.inputState(.valid)
}
.padding()
.background(.coreEcoColor)
}
}
66 changes: 66 additions & 0 deletions Backpack-SwiftUI/TextField/Classes/BPKTextFieldState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018-2022 Skyscanner Ltd
*
* 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 {
/// The state of the text field.
public enum State {
/// The default state.
case `default`
/// Disables the text field.
case disabled
/// Adds a clear button to the text field. The action is called when the button is tapped.
case clear(accessibilityLabel: String, action: () -> Void)
/// Adds a valid icon to the text field.
case valid
/// Adds an error icon to the text field and changes the border color to error.
case error

var borderColor: BPKColor {
switch self {
case .disabled: return .surfaceHighlightColor
case .error: return .statusDangerSpotColor
default: return .lineColor
}
}

var isDisabled: Bool {
switch self {
case .disabled: return true
default: return false
}
}

var textColor: BPKColor {
switch self {
case .disabled: return .textDisabledColor
default: return .textPrimaryColor
}
}

var icon: BPKTextField.Icon? {
switch self {
case .clear: return BPKTextField.Icon(icon: .closeCircle, color: .textSecondaryColor)
case .valid: return BPKTextField.Icon(icon: .tickCircle, color: .statusSuccessSpotColor)
case .error: return BPKTextField.Icon(icon: .exclamationCircle, color: .statusDangerSpotColor)
default: return nil
}
}
}
}
47 changes: 47 additions & 0 deletions Backpack-SwiftUI/TextField/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Backpack-SwiftUI/TextField

[![Cocoapods](https://img.shields.io/cocoapods/v/Backpack-SwiftUI.svg?style=flat)](hhttps://cocoapods.org/pods/Backpack-SwiftUI)
[![class reference](https://img.shields.io/badge/Class%20reference-iOS-blue)](https://backpack.github.io/ios/versions/latest/swiftui/Structs/BPKTextField.html)
[![view on Github](https://img.shields.io/badge/Source%20code-GitHub-lightgrey)](https://github.com/Skyscanner/backpack-ios/tree/main/Backpack-SwiftUI/TextField)

| Day | Night |
| --- | --- |
| <img src="https://raw.githubusercontent.com/Skyscanner/backpack-ios/main/screenshots/iPhone-swiftui_text-field___default_lm.png" alt="" width="375" /> |<img src="https://raw.githubusercontent.com/Skyscanner/backpack-ios/main/screenshots/iPhone-swiftui_text-field___default_dm.png" alt="" width="375" /> |

# Usage

Create a `BPKTextField` and bind the `text` property to a `Binding<String>`.

```swift
@State var text: String = ""

BPKTextField(text: $text)
```

### Setting a placeholder

```swift
BPKTextField("Placeholder", text: $text)
```

### Changing the State

```swift
BPKTextField(text: $text)
.inputState(.error)
.inputState(.valid)
```

### Adding a clear button

```swift
@State var text: String = "some text"

BPKTextField(text: $text)
.inputState(
.clear(
accessibilityLabel: "Clear",
action: { text = "" }
)
)
```
47 changes: 47 additions & 0 deletions Backpack-SwiftUI/Utils/Classes/Outline.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018-2022 Skyscanner Ltd
*
* 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

// Helper functions to apply the outline style
struct Outline: ViewModifier {
let color: BPKColor
let cornerRadius: BPKCornerRadius

func body(content: Content) -> some View {
return content
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(Color(color))
)
}
}

extension View {
@ViewBuilder
func outline(_ color: BPKColor?, cornerRadius: BPKCornerRadius) -> some View {
if let color = color {
ModifiedContent(
content: self,
modifier: Outline(color: color, cornerRadius: cornerRadius)
)
} else {
self
}
}
}
5 changes: 5 additions & 0 deletions Example/Backpack Screenshot/SwiftUIScreenshots.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ class SwiftUIScreenshots: BackpackSnapshotTestCase {
saveScreenshot(component: "skeleton", scenario: "default", userInterfaceStyle: userInterfaceStyle)
}

navigate(title: "Text fields") {
switchTab(title: "SwiftUI")
saveScreenshot(component: "text-field", scenario: "default", userInterfaceStyle: userInterfaceStyle)
}

navigate(title: "Price") {
switchTab(title: "SwiftUI")

Expand Down

0 comments on commit 1944985

Please sign in to comment.