Extensions

View+Extensions.swift

Consistency is the difference between a UI that feels designed and one that feels assembled. View+Extensions.swift is ShipThatApp's answer to text-field consistency: instead of copy-pasting padding, borders, and keyboard behavior onto every TextField and SecureField, you apply one modifier — .inputTextFieldStyling() — and every input field across the sign-in and registration screens looks and behaves identically.

It bundles the boring-but-easy-to-get-wrong details (autocapitalization off, autocorrection off, a properly inset rounded border) into a single, named ViewModifier. Change the look in one place and the whole app updates.

Location: ShipThatApp/Utils/Extensions/View+Extensions.swift

What it provides

The InputTextFieldStyling modifier

A ViewModifier that wraps any view with the app's standard text-input chrome. This is the exact implementation that ships:

/// A custom view modifier that applies styling to a text field
struct InputTextFieldStyling: ViewModifier {
  func body(content: Content) -> some View {
    content
      .padding()
      .overlay {
        RoundedRectangle(cornerRadius: 8)
          .stroke(Color(uiColor: .secondaryLabel), lineWidth: 1)
      }
      .textInputAutocapitalization(.never)
      .autocorrectionDisabled()
  }
}

Three details make this better than a naive border:

  • The border is an .overlay, not a .border. A RoundedRectangle stroke drawn as an overlay hugs the padded content and gives true rounded corners — .border can't round.
  • It uses a semantic color. Color(uiColor: .secondaryLabel) adapts automatically to light and dark mode and to accessibility contrast settings, so the field never looks wrong on a dark background.
  • It fixes input behavior, not just looks. .textInputAutocapitalization(.never) and .autocorrectionDisabled() are exactly what you want for emails and passwords — no surprise capitalization, no "helpful" autocorrect mangling a credential.

The .inputTextFieldStyling() entry point

A View extension so you never instantiate the modifier by hand:

extension View {
  func inputTextFieldStyling() -> some View {
    self.modifier(InputTextFieldStyling())
  }
}

How to use it

Chain it onto any input field. That's the entire API:

TextField("Your email address", text: $email)
  .inputTextFieldStyling()

In the live app, SignInOptions uses it to style both the email and password fields, layering field-specific keyboard hints before the shared styling. This is the actual call site:

TextField("Your email address", text: $email)
  .keyboardType(.emailAddress)
  .textContentType(.emailAddress)
  .autocorrectionDisabled()
  .textInputAutocapitalization(.never)
  .inputTextFieldStyling()
  .padding(.horizontal)

if !isMagicLinkSelected {
  SecureField("Your password", text: $password)
    .autocorrectionDisabled()
    .textInputAutocapitalization(.never)
    .inputTextFieldStyling()
    .padding(.horizontal)
}

RegistrationView follows the same pattern for its email and password inputs. The result: every credential field in the app shares one border, one corner radius, and one set of keyboard rules.

Where this fits

These fields feed the Authentication Flow. The screens that apply the modifier are SignInView and its SignInViewModel; the email string they collect is validated by String+Extensions's isValidEmail().

Customize / extend it

This file is the home for your app-wide visual vocabulary. To rebrand every input field, edit InputTextFieldStyling.body(content:) once — for example, give it your accent color and a softer corner:

struct InputTextFieldStyling: ViewModifier {
  func body(content: Content) -> some View {
    content
      .padding()
      .background(Color(uiColor: .secondarySystemBackground), in: .rect(cornerRadius: 12))
      .overlay {
        RoundedRectangle(cornerRadius: 12)
          .stroke(Color.accentColor.opacity(0.4), lineWidth: 1)
      }
      .textInputAutocapitalization(.never)
      .autocorrectionDisabled()
  }
}

The same file is the natural place to grow a small library of reusable modifiers. A primary-button style is a common next addition:

struct PrimaryButtonStyling: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(.title3.weight(.semibold))
      .frame(maxWidth: .infinity, minHeight: 50)
      .background(.accent, in: .rect(cornerRadius: 8))
      .foregroundStyle(.white)
  }
}

extension View {
  func primaryButtonStyling() -> some View {
    modifier(PrimaryButtonStyling())
  }
}

Modifier order matters

.inputTextFieldStyling() applies .padding() first, then draws its border around that padded frame. If you add your own .padding() after the modifier, you'll push the border inward and break the visual. Put field-specific modifiers (.keyboardType, .textContentType) before .inputTextFieldStyling() and outer layout .padding(.horizontal) after it — exactly as SignInOptions does.

Previous
String+Extensions