All Articles
CategorySwiftUI
Reading Time
8 min read
Published
2026-10-15
Word Count
1,540words

Grab a coffee — this one is a deep dive!

SwiftUI Components 2.1: ready-made views and a time limit for untrusted patterns

Summary

How SwiftUI-Components keeps its views plain, moves logic into tested value types, and bounds regular expressions that arrive in a JSON form.

  • SwiftUI-Components is a Swift package of ready-made SwiftUI views for iOS 17 and macOS 14: buttons, cards, charts, form inputs with validation, gestures, lists, overlays, loaders and toasts.
  • Components keep their state in @State and @Binding.
  • Add the package in Xcode with File › Add Package Dependencies… and the URL https://github.com/muhittincamdali/SwiftUI-Components.git, or in Package.swift with from: "2.1.0".
  • The README's roadmap lists the next steps: the Swift Package Index (.spi.yml is already in the repository), tests for individual views starting with the form inputs, and conditional fields in FormSchema.
SwiftUI Components 2.1: ready-made views and a time limit for untrusted patterns

SwiftUI-Components is a Swift package of ready-made SwiftUI views for iOS 17 and macOS 14:

buttons, cards, charts, form inputs with validation, gestures, lists, overlays, loaders and

toasts. Version 2.1.0 was released on 14 October 2026. This article covers the problem the

package solves, the design choices behind it, one mistake from its commit history, how to

use it, and what it does not do.

Table of Contents


The problem

Most apps need the same handful of views: a button that shows a loading state, a card, a

small chart, a field that checks an email address, a toast. Writing them is not hard, but

the details take time, and the bugs live in the logic rather than in the layout: a card-number

field that crashes on an unexpected character, a toast that is hidden by the timer of the

previous one, a slider that crashes when its step is zero.

The package is for developers who want a working component today. Every component is a plain

SwiftUI View configured through its initializer. There is no theme system to adopt and no

architecture to follow, so you can take one or two views and ignore the rest. The package has

no dependencies.

Its history is short and shows up in the version numbers. The repository was opened in

February 2026, and 1.0.0 was tagged on the first commit. In June 2026 the components were

removed and replaced with placeholder code (commit c7b3799). On 14 October 2026 they were

restored as 2.0.0, made to build for iOS and macOS in the Swift 6 language mode, and fixed

where new tests found bugs. Hours later, 2.1.0 added form validation, forms from JSON,

gestures and a contrast checker, rewritten from three of the author's older packages

(SwiftUI-Forms-Kit, SwiftUI-Gesture-Library and iOSUIComponents), which are now archived.

The README and the CHANGELOG describe this history in the same terms.

Design decisions and alternatives

Plain views, logic in value types. Components keep their state in @State and

@Binding. The only public reference types are SnackBarManager, BannerManager,

PaginatedListViewModel and AsyncValidation, all on the main actor. Decisions that do not

need a screen live in value types or static functions: Validator, FormSchema, StepFlow,

SwipeDirection, SwipeSequenceMatcher, TransformState and UnistrokeRecognizer, as well as

helpers such as CreditCardInput.isLuhnValid(_:) and FlowLayout.arrange(sizes:maxWidth:spacing:).

The alternative was to test views directly with a view-inspection library. That would add a

dependency to the test target, and a static function is faster and simpler to test. The cost

is stated in the README: most individual views do not have their own tests.

Platform differences in one place, almost. Extensions/Platform.swift maps almost all

UIKit-only APIs (system colors, haptics, keyboard type, capitalization, wheel pickers) to

AppKit or to no-ops. Spreading #if os(...) through every component would have worked too,

but it makes each file harder to read. Because of the shim, TextInputField(keyboardType:)

takes the package's own KeyboardKind enum instead of UIKeyboardType. There are two

exceptions. TestimonialCarousel has its own #if os(iOS) branch: macOS has no paged

TabView, so it shows one testimonial at a time. And cornerRadius(_:corners:) is the one

public API that takes a UIKit type, UIRectCorner, so it exists only on iOS.

Claim only what is built. The 2.0.0 manifest dropped tvOS, watchOS and visionOS because

the package had never been built for them, and moved to swift-tools-version 6.2, the

toolchain CI actually uses. Raising the minimum to iOS 17 and macOS 14 let the restored code

use onChange(of:initial:_:) without #available branches. It was a breaking change, which

is why that release is 2.0.0.

Render checks instead of snapshot files. RenderTests and GaugeNeedleTests render views

with ImageRenderer. The gallery pages are rendered in light and dark mode, which catches

views that crash or collapse to an empty size, and the gauge test reads pixels to check where

the needle points. A pixel-by-pixel snapshot library would add a dependency and break on

anti-aliasing differences between OS versions. The trade-off is that these checks do not

catch color or layout drift.

Samples that compile. The README's code samples are copied from

Examples/ComponentGallery/ComponentGallery/ReadmeSamples.swift. The example app compiles that

file, and so does the test target through a symlink, where a render test also draws it. If a

sample drifts from the API, the build fails.

A real mistake: screening patterns instead of bounding the engine

FormSchema builds a form from JSON, and that JSON usually comes from a server. A field can

carry a pattern, so the pattern text is untrusted input. A backtracking regular expression

engine can take exponential time on patterns such as (a|a)+. The commit that brought JSON

forms into the package (697c48f) records the measurement for Swift's Regex, which cannot be

cancelled: (a|a)+ took 5 seconds on 24 "a" characters followed by "!".

The first answer, in that same commit, was a screen called PatternSafety. It read the pattern

before running it, rejected nested repetition and ambiguous alternation, and estimated the worst

case for a 256-character value. The commit message already said that the screen used its own

reading of the pattern, not the engine's parser.

That gap was the bug. As the follow-up commit e471e0b records, FormSchema.decode accepted

\e{(a|a)+} through the public API alone: the screen took the braces as part of the escape,

the engine did not, and errors(in:) then ran for 29.6 seconds on a short input, doubling with

every extra "a". The 256 limit also counted Characters while the engine's work grows with

code units, so \w*\w*\w*z passed the screen and took 7.4 seconds.

The fix did not patch the screen. No scan of the pattern text can tell every slow pattern from

a fast one, so e471e0b removed it and bounded the engine instead. PatternMatcher is now the

only code that runs a regular expression, and Validator.matches and FormSchema both call it:

  • Patterns are compiled and run by NSRegularExpression, so the pattern that is accepted is

exactly the pattern that runs.

  • Matching uses .reportProgress; a match is stopped after 20 ms or at the caller's deadline,

and it fails. A pattern stopped by its own limit is remembered and fails later values without

running.

  • Text longer than 1,000 UTF-16 code units fails without being matched, and all matches in one

errors(in:) call share a 100 ms budget.

  • The schema itself has resource limits: JSON over 1 MB, more than 200 fields, more than 50

pattern fields or more than 500 options are rejected.

A later commit, 803ee6b, closed a smaller gap: after the budget was spent, each remaining

field still compiled its pattern before failing. The deadline check now comes before compiling.

The screen never shipped in a release; it was written and removed inside PR #6. The lesson is

general: if a filter reads input with a different parser from the engine that will run it, the

two can disagree, so the limit belongs in the engine.

Usage

Add the package in Xcode with File › Add Package Dependencies… and the URL

https://github.com/muhittincamdali/SwiftUI-Components.git, or in Package.swift with

from: "2.1.0". The quick start from the README replaces ContentView.swift in a new app:

swift
1import SwiftUI
2import SwiftUIComponents
3 
4struct ContentView: View {
5 @State private var rating = 3.0
6 
7 var body: some View {
8 VStack(spacing: 20) {
9 Text("Rate this app")
10 .font(.headline)
11 RatingInput(rating: $rating)
12 PrimaryButton("Send rating") {
13 print("Rating: \(rating)")
14 }
15 }
16 .padding()
17 }
18}

A form described as JSON is decoded with checks for duplicate ids, impossible limits, invalid

regular expressions and the size limits above:

swift
1func loadContactSchema() throws -> FormSchema {
2 try FormSchema.decode(from: Data(contactFormJSON.utf8)) // throws for malformed JSON
3}

A validated field uses ValidatedValue, which shows an error only after the user edits the

field or submits:

swift
1@State private var email = ValidatedValue("", .required().and(.email()))

The README's Usage section has the complete samples for buttons, charts, the payment form,

snackbars, FlowLayout, the JSON form, gestures and ColorContrast.

Limits

  • Most individual views do not have their own tests. The 163 Swift Testing tests cover logic,

state managers, edge-case inputs and the gallery pages; render tests check size and crashes,

not appearance.

  • On macOS there are no haptics, keyboard type and capitalization settings are ignored, and

wheel pickers become steppers or menus. cornerRadius(_:corners:) is iOS-only. The Platform

Differences article lists each case.

  • CreditCardInput checks format, the Luhn digit, expiry and CVV length. That is input

validation, not payment verification.

  • The pattern time limit is wall-clock time, so on a heavily loaded device a legitimate but

expensive pattern can time out. Patterns use ICU syntax, not Swift Regex.

  • .snackBarHost() and .bannerHost() use the shared managers; there is no way yet to pass

your own instance to them.

  • The DocC site leaves out the members each view inherits from View. Including them made the

archive 379,410 files (2.5 GB), too large for GitHub Pages (c896b96).

The README's roadmap lists the next steps: the Swift Package Index (.spi.yml is already in the

repository), tests for individual views starting with the form inputs, and conditional fields in

FormSchema.

Source code: github.com/muhittincamdali/SwiftUI-Components

Tags

#swiftui#swift-package#form-validation#gestures#testing
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

Senior Mobile Engineer with 12+ years of experience. I build mobile products with iOS, Flutter and React Native, working full-stack and AI-native.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy

Share