A comprehensive guide to using the GoodSwiftUI library in your iOS projects.
Note: This guide includes simplified helper patterns to reduce cognitive load and speed up development.
- Open your project in Xcode
- Go to File → Add Packages...
- Enter the repository URL:
https://github.com/GoodRequest/GoodSwiftUI - Select the version and add the package
import PackageDescription
let package = Package(
name: "YourProject",
dependencies: [
.package(url: "https://github.com/GoodRequest/GoodSwiftUI", from: "1.0.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "GoodSwiftUI", package: "GoodSwiftUI"),
// Or import individual modules:
// .product(name: "GRButton", package: "GoodSwiftUI"),
// .product(name: "GRInputField", package: "GoodSwiftUI"),
// .product(name: "GRToggle", package: "GoodSwiftUI"),
// .product(name: "GRAsyncImage", package: "GoodSwiftUI"),
]
)
]
)Get up and running in under 5 minutes with this copy-paste setup:
// AppearanceConfig.swift
import GRButton
import GRToggle
import GRInputField
struct AppearanceConfig {
static func configure() {
InputFieldView.configureAppearance()
}
}
// MARK: - Helper Functions (Reduces 9 parameters to 2)
extension GRButtonAppearanceModel {
static func filled(background: Color, foreground: Color, font: Font = .body.weight(.semibold)) -> Self {
.init(
backgroundColor: background,
disabledBackgroundColor: background.opacity(0.4),
loadingTintColor: foreground,
iconTintColor: foreground,
iconDisabledTintColor: foreground.opacity(0.4),
textColor: foreground,
disabledTextColor: foreground.opacity(0.4),
textFont: font,
disabledTextFont: font
)
}
static func outlined(tint: Color, font: Font = .body) -> Self {
.init(
backgroundColor: .clear,
disabledBackgroundColor: .clear,
loadingTintColor: tint,
iconTintColor: tint,
iconDisabledTintColor: tint.opacity(0.4),
textColor: tint,
disabledTextColor: tint.opacity(0.4),
textFont: font,
disabledTextFont: font
)
}
}
// MARK: - Appearance Definitions (Now just 2 parameters!)
extension GRButtonAppearanceModel {
static let primary = .filled(background: .blue, foreground: .white)
static let secondary = .outlined(tint: .blue)
}
extension GRToggleAppearance {
static let `default` = GRToggleAppearance(
tintColor: .blue,
uncheckedBorderColor: .gray,
checkedBackgroundColor: .blue.opacity(0.2),
checkmarkImageTintColor: .white,
checkmarkImage: Image(systemName: "checkmark")
)
}@main
struct MyApp: App {
init() {
AppearanceConfig.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}import SwiftUI
import GRButton
import GRInputField
struct ContentView: View {
@State private var email = ""
var body: some View {
VStack(spacing: 20) {
InputField(text: $email, title: "Email", placeholder: "email@example.com")
Button("Submit") { }
.buttonStyle(GRButtonStyle(appearance: .primary, size: .large(stretch: false)))
}
.padding()
}
}Total setup time: Under 5 minutes ✅
A highly customizable button component with support for icons, loading states, and various sizes.
| SwiftUI Concept | GRButton Property |
|---|---|
.foregroundColor() |
textColor |
.background() |
backgroundColor |
.font() |
textFont |
.disabled(true) |
disabledTextColor / disabledBackgroundColor |
| Icon tint | iconTintColor |
| Loading indicator | loadingTintColor |
import SwiftUI
import GRButton
Button("Click Me") {
// Handle action
}
.buttonStyle(GRButtonStyle(appearance: .primary, size: .medium(stretch: false)))// Text buttons
.small(stretch: false) // Height: ~36pt
.medium(stretch: false) // Height: ~44pt
.large(stretch: false) // Height: ~52pt
// Full-width buttons
.large(stretch: true) // Takes full width
// Icon-only buttons (requires EmptyView label)
Button(action: {}) { EmptyView() }
.buttonStyle(GRButtonStyle(
appearance: .primary,
iconModel: .init(rightIcon: Image(systemName: "plus")),
size: .circleSmall() // 36x36pt
))
.circleMedium() // 44x44pt
.square() // 44x44pt square// Left icon
Button("Continue") { }
.buttonStyle(GRButtonStyle(
appearance: .primary,
iconModel: .init(leftIcon: Image(systemName: "arrow.right")),
size: .medium(stretch: false)
))
// Both icons
Button("Share") { }
.buttonStyle(GRButtonStyle(
appearance: .secondary,
iconModel: .init(
leftIcon: Image(systemName: "square.and.arrow.up"),
rightIcon: Image(systemName: "chevron.right")
),
size: .large(stretch: false)
))@State private var isLoading = false
Button("Submit") {
isLoading.toggle()
}
.buttonStyle(GRButtonStyle(
appearance: .primary,
isLoading: isLoading,
size: .medium(stretch: false)
))// Instead of configuring 9 parameters, use helpers:
extension GRButtonAppearanceModel {
static let custom = .filled(background: .green, foreground: .white)
static let customOutlined = .outlined(tint: .green)
}Advanced input field component with validation, formatting, and rich customization options. Supports both SwiftUI and UIKit.
Important: Call
InputFieldView.configureAppearance()before using input fields.
import SwiftUI
import GRInputField
struct MyView: View {
@State private var text = ""
init() {
InputFieldView.configureAppearance()
}
var body: some View {
InputField(
text: $text,
title: "Email",
placeholder: "Enter your email",
hint: "We'll never share your email"
)
}
}// Step 1: Define custom validation errors
enum RegistrationError: ValidationError {
case invalidEmail
case passwordTooShort
var errorDescription: String? {
switch self {
case .invalidEmail:
return "Please enter a valid email address"
case .passwordTooShort:
return "Password must be at least 8 characters"
}
}
}
// Step 2: Apply validation
struct RegistrationView: View {
@State private var email = ""
@State private var validityGroup = ValidityGroup()
var body: some View {
InputField(
text: $email,
title: "Email",
placeholder: "email@example.com"
)
.validationCriteria {
Criterion.matches("[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}")
.failWith(error: RegistrationError.invalidEmail)
}
.validityGroup($validityGroup)
Button("Validate All Fields") {
validityGroup.validateAll()
if validityGroup.allValid() {
// Proceed
}
}
}
}@State private var password = ""
InputField(
text: $password,
title: "Password",
hint: "At least 8 characters"
)
.inputFieldTraits(
returnKeyType: .done,
isSecureTextEntry: true
)
.setEyeButtonAccessibilityLabel(
showLabel: "Show password",
hideLabel: "Hide password"
)@State private var percent: Double = 0.5
InputField(
value: $percent,
format: .percent.precision(.fractionLength(0..<2)),
title: "Discount (%)",
placeholder: "0 %"
)
.inputFieldTraits(keyboardType: .numbersAndPunctuation)@State private var phoneNumber = ""
InputField(
text: $phoneNumber,
title: "Phone Number",
placeholder: "123456789",
leftView: {
Text("+1")
.foregroundColor(.gray)
.padding(.leading, 8)
},
rightView: {
Button {
phoneNumber = ""
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray)
}
}
)@State private var password = ""
@State private var validityGroup = ValidityGroup()
InputField(
text: $password,
title: "Password",
placeholder: "Enter password"
)
.validationCriteria {
// Minimum length
Criterion { $0?.count ?? 0 >= 8 }
.failWith(error: RegistrationError.passwordTooShort)
.realtime()
// Must contain uppercase
Criterion { $0?.range(of: "[A-Z]", options: .regularExpression) != nil }
.failWith(error: RegistrationError.noUppercase)
.realtime()
// Must contain number
Criterion { $0?.range(of: "[0-9]", options: .regularExpression) != nil }
.failWith(error: RegistrationError.noNumber)
.realtime()
}
.validityGroup($validityGroup)enum FormFields: Int, CaseIterable, Hashable {
case email, password, confirmPassword
}
@FocusState private var focusedField: FormFields?
InputField(text: $email, title: "Email")
.bindFocusState($focusedField, to: .email)
.inputFieldTraits(returnKeyType: .next)
InputField(text: $password, title: "Password")
.bindFocusState($focusedField, to: .password)
.inputFieldTraits(returnKeyType: .next)
InputField(text: $confirmPassword, title: "Confirm Password")
.bindFocusState($focusedField, to: .confirmPassword)
.inputFieldTraits(returnKeyType: .done)InputField(text: $text, title: "Username")
// Keyboard configuration
.inputFieldTraits(
textContentType: .username,
keyboardType: .emailAddress,
returnKeyType: .done,
autocapitalizationType: .none,
autocorrectionType: .no,
clearButtonMode: .whileEditing,
isSecureTextEntry: false,
hapticsAllowed: true
)
// Input restrictions (regex)
.allowedInput("^[a-zA-Z0-9_]{0,20}$")
// Accessibility (5 methods available)
.setAccessibilityLabel("Username input field")
.setAccessibilityIdentifier("usernameTextField")
.setAccessibilityHint("Enter your username")
.setAccessibilityValue(text)
// Validation
.validationCriteria { /* ... */ }
.validityGroup($validityGroup)
// Focus
.bindFocusState($focusState, to: .username)
// Actions
.onSubmit { print("Submitted") }
.onResign { print("Resigned") }
.onEditingChanged { print("Changed") }
// Custom appearance
.inputFieldAppearance(.custom)
// State
.disabled(false)extension InputFieldAppearance {
static let custom = InputFieldAppearance(
titleFont: .systemFont(ofSize: 14, weight: .medium),
titleColor: .label,
textFieldTintColor: .systemBlue,
textFieldFont: .systemFont(ofSize: 16),
hintFont: .systemFont(ofSize: 12),
borderWidth: 1,
cornerRadius: 8,
height: 56,
eyeImageHidden: UIImage(systemName: "eye.slash"),
eyeImageVisible: UIImage(systemName: "eye"),
enabled: .default,
selected: InputFieldViewStateAppearance(
borderColor: .systemBlue,
contentBackgroundColor: .systemBackground
),
disabled: .default,
failed: InputFieldViewStateAppearance(
borderColor: .systemRed,
hintColor: .systemRed
)
)
}
// Usage
InputField(text: $text, title: "Custom")
.inputFieldAppearance(.custom)@State private var validityGroup = ValidityGroup()
var body: some View {
VStack {
// Your input fields with .validityGroup($validityGroup)
// Check if all fields are valid
if validityGroup.allValid() {
Text("Form is valid ✓").foregroundColor(.green)
}
// Force validation on all fields
Button("Submit") {
validityGroup.validateAll()
if validityGroup.allValid() {
// Process form
submitForm()
}
}
// Clear validation messages
Button("Clear Validation") {
validityGroup.removeAll()
}
}
}Input fields support comprehensive accessibility:
InputField(text: $username, title: "Username")
// For screen readers
.setAccessibilityLabel("Username input")
// For UI testing
.setAccessibilityIdentifier("usernameField")
// Provide additional context
.setAccessibilityHint("Enter your username between 3-20 characters")
// Dynamic value for screen readers
.setAccessibilityValue(username)
// Secure field eye button labels
.setEyeButtonAccessibilityLabel(
showLabel: "Show password",
hideLabel: "Hide password"
)Custom toggle styles including switches, checkboxes, and radio buttons.
import SwiftUI
import GRToggle
@State private var isEnabled = false
// Standard sizes
Toggle(isOn: $isEnabled) {
Text("Enable notifications")
}
.toggleStyle(GRSwitchStyle(appearance: .default, size: .default))
Toggle(isOn: $isEnabled) {
Text("Small switch")
}
.toggleStyle(GRSwitchStyle(appearance: .default, size: .small))
Toggle(isOn: $isEnabled) {
Text("Large switch")
}
.toggleStyle(GRSwitchStyle(appearance: .default, size: .large))New Feature: Control toggle position with alignment parameter
// Toggle on the right (default)
Toggle(isOn: $isEnabled) {
Text("Enable notifications")
}
.toggleStyle(GRSwitchStyle(
appearance: .default,
size: .large,
alignment: .trailing // Toggle on right (default)
))
// Toggle on the left
Toggle(isOn: $isEnabled) {
Text("Enable notifications")
}
.toggleStyle(GRSwitchStyle(
appearance: .default,
size: .large,
alignment: .leading // Toggle on left
))@State private var isChecked = false
Toggle(isOn: $isChecked) {
Text("I agree to terms and conditions")
}
.toggleStyle(GRToggleStyle(
appearance: .default,
style: .checkbox,
size: .large,
alignment: .trailing // Optional alignment
))@State private var selectedOption = false
Toggle(isOn: $selectedOption) {
Text("Option A")
}
.toggleStyle(GRToggleStyle(
appearance: .default,
style: .radio,
size: .large
))enum PaymentMethod: String, CaseIterable {
case card = "Credit Card"
case paypal = "PayPal"
case crypto = "Cryptocurrency"
}
@State private var selectedMethod: PaymentMethod = .card
VStack(alignment: .leading, spacing: 12) {
ForEach(PaymentMethod.allCases, id: \.self) { method in
Toggle(isOn: Binding(
get: { selectedMethod == method },
set: { if $0 { selectedMethod = method } }
)) {
Text(method.rawValue)
}
.toggleStyle(GRToggleStyle(
appearance: .default,
style: .radio,
size: .large
))
}
}@State private var isSelected = false
Toggle(isOn: $isSelected) {
Text("Select item")
}
.toggleStyle(GRToggleStyle(
appearance: .default,
style: .circularCheck,
size: .large
))extension GRToggleAppearance {
static let custom = GRToggleAppearance(
tintColor: .green,
uncheckedBorderColor: .gray,
checkedBackgroundColor: .green.opacity(0.2),
checkmarkImageTintColor: .white,
checkmarkImage: Image(systemName: "checkmark"),
disabledOpacity: 0.3
)
}
Toggle(isOn: $isChecked) {
Text("Custom checkbox")
}
.toggleStyle(GRToggleStyle(
appearance: .custom,
style: .checkbox,
size: .large
))Asynchronous image loading component with built-in caching.
import SwiftUI
import GRAsyncImage
GRAsyncImage(url: URL(string: "https://example.com/image.jpg"))
.frame(width: 200, height: 200)
.cornerRadius(12)GRAsyncImage(
url: URL(string: "https://example.com/image.jpg"),
loadingPlaceholder: {
ZStack {
Color.gray.opacity(0.2)
ProgressView()
}
},
failurePlaceholder: {
ZStack {
Color.gray.opacity(0.2)
VStack {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
Text("Failed to load image")
.font(.caption)
.foregroundColor(.red)
}
}
}
)
.frame(width: 300, height: 200)
.cornerRadius(12)List(items) { item in
HStack(spacing: 12) {
GRAsyncImage(url: item.thumbnailURL)
.frame(width: 60, height: 60)
.cornerRadius(8)
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.subtitle)
.font(.caption)
.foregroundColor(.secondary)
}
}
}LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 16) {
ForEach(images) { image in
GRAsyncImage(url: image.url)
.frame(width: 150, height: 150)
.aspectRatio(contentMode: .fill)
.clipped()
.cornerRadius(12)
}
}A modifier that constrains content to a readable width, particularly useful for iPad and larger screens.
import SwiftUI
import GoodSwiftUI
struct MyView: View {
var body: some View {
VStack {
Text("This text will be constrained to a readable width")
// More content...
}
.fittingReadableWidth()
}
}VStack(alignment: .leading, spacing: 16) {
Text("Left-aligned content")
.font(.title)
Text("Also left-aligned")
Text("All within readable width")
}
.fittingReadableWidth(alignment: .leading)FittingReadableWidth(alignment: .center) {
VStack(spacing: 20) {
Text("Centered content")
Text("Within readable width")
}
}ScrollView {
VStack(spacing: 20) {
// Long-form content benefits from readable width
Text("Article Title")
.font(.largeTitle)
.bold()
Text("""
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Long paragraphs are easier to read when constrained to
a reasonable width, especially on iPad and larger displays.
""")
.font(.body)
Image("article-image")
.resizable()
.aspectRatio(contentMode: .fit)
}
.padding()
.fittingReadableWidth()
}Instead of configuring 9 parameters for each button appearance, use helper functions:
// Add these helper functions once to your project
extension GRButtonAppearanceModel {
/// Creates a filled button appearance (9 params → 2 params)
static func filled(
background: Color,
foreground: Color,
font: Font = .body.weight(.semibold)
) -> Self {
.init(
backgroundColor: background,
disabledBackgroundColor: background.opacity(0.4),
loadingTintColor: foreground,
iconTintColor: foreground,
iconDisabledTintColor: foreground.opacity(0.4),
textColor: foreground,
disabledTextColor: foreground.opacity(0.4),
textFont: font,
disabledTextFont: font
)
}
/// Creates an outlined button appearance (9 params → 2 params)
static func outlined(
tint: Color,
font: Font = .body
) -> Self {
.init(
backgroundColor: .clear,
disabledBackgroundColor: .clear,
loadingTintColor: tint,
iconTintColor: tint,
iconDisabledTintColor: tint.opacity(0.4),
textColor: tint,
disabledTextColor: tint.opacity(0.4),
textFont: font,
disabledTextFont: font
)
}
}
// Now defining appearances is simple!
extension GRButtonAppearanceModel {
static let primary = .filled(background: .blue, foreground: .white)
static let secondary = .outlined(tint: .blue)
static let danger = .filled(background: .red, foreground: .white)
}For GRInputField, call InputFieldView.configureAppearance() early in your app lifecycle:
@main
struct MyApp: App {
init() {
InputFieldView.configureAppearance()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Define your app's button, toggle, and input field appearances in a centralized file:
// AppearanceExtensions.swift
import GRButton
import GRToggle
// Use helper functions for simplicity
extension GRButtonAppearanceModel {
static let primary = .filled(background: .accentColor, foreground: .white)
static let secondary = .outlined(tint: .accentColor)
}
extension GRToggleAppearance {
static let `default` = GRToggleAppearance(
tintColor: .accentColor,
uncheckedBorderColor: .gray,
checkedBackgroundColor: .accentColor.opacity(0.2),
checkmarkImageTintColor: .white,
checkmarkImage: Image(systemName: "checkmark")
)
}Group related input fields using ValidityGroup for coordinated validation:
@State private var validityGroup = ValidityGroup()
Form {
InputField(text: $email, title: "Email")
.validityGroup($validityGroup)
.validationCriteria { /* ... */ }
InputField(text: $password, title: "Password")
.validityGroup($validityGroup)
.validationCriteria { /* ... */ }
}
Button("Submit") {
validityGroup.validateAll()
if validityGroup.allValid() {
submitForm()
}
}Use @FocusState with input fields for smooth keyboard navigation:
enum FormField: Int, CaseIterable, Hashable {
case firstName, lastName, email
}
@FocusState private var focusedField: FormField?
// Each field
InputField(text: $firstName, title: "First Name")
.bindFocusState($focusedField, to: .firstName)
.inputFieldTraits(returnKeyType: .next)For lists with many images, ensure frames are set to optimize caching:
LazyVStack {
ForEach(items) { item in
GRAsyncImage(url: item.imageURL)
.frame(width: 300, height: 200) // Consistent sizing helps caching
.cornerRadius(12)
}
}Import only the components you need to reduce compilation time:
import GRButton // Only buttons
import GRInputField // Only input fields
// vs
import GoodSwiftUI // All componentsAlways set accessibility labels for better accessibility:
InputField(text: $username, title: "Username")
.setAccessibilityLabel("Username input field")
.setAccessibilityIdentifier("usernameField") // For UI tests
.setAccessibilityHint("Enter your username")
Button("Submit") { }
.buttonStyle(GRButtonStyle(appearance: .primary))
.accessibilityLabel("Submit form")Use accessibility identifiers for UI testing:
InputField(text: $text, title: "Email")
.setAccessibilityIdentifier("emailTextField")
// In UI tests
let emailField = app.textFields["emailTextField"]
emailField.tap()
emailField.typeText("test@example.com")✅ Use GoodSwiftUI when:
- Building a design system with consistent styling
- Need complex form validation
- Require custom button states (loading, disabled, icons)
- Want standardized UI components across a large codebase
- Building enterprise/team projects where consistency matters
- Building a simple MVP or prototype
- Need a one-off custom component
- Working on a small personal project
- Need the fastest possible implementation
- GitHub Repository
- Sample Project
- AI Usage Rules - For AI assistants
- Issue Tracker
GoodSwiftUI is released under the MIT license. See LICENSE for details.