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
- Design decisions and alternatives
- A real mistake: screening patterns instead of bounding the engine
- Usage
- Limits
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:
1import SwiftUI2import SwiftUIComponents3 4struct ContentView: View {5 @State private var rating = 3.06 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:
1func loadContactSchema() throws -> FormSchema {2 try FormSchema.decode(from: Data(contactFormJSON.utf8)) // throws for malformed JSON3}A validated field uses ValidatedValue, which shows an error only after the user edits the
field or submits:
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.
CreditCardInputchecks 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
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

