SwiftUI Row Component
A list row: optional leading symbol, title, optional subtitle, and whatever you want on the trailing edge.
Wi-FiNorth YardOn
Storage42 GB of 128 GB
Sign Out
Variants: Plain, Tappable
Install the free SwiftUI row
- Add SwiftKitUI.swift to your app once. It holds the tokens and motion curves.
- Copy SKRow.swift below into your project. It needs nothing else.
Usage
Usage.swift
SKRow(title: "Wi-Fi", subtitle: "Home", systemImage: "wifi") { Text("On") }
SKRow(title: "Account", systemImage: "person.fill", onTap: { })
SKRow(title: "Last", showsSeparator: false)Full source · 145 lines · MIT
SKRow.swift
//
// SKRow.swift
// Swift Kit UI
//
// Requires: SwiftKitUI.swift
//
// A list row: optional leading symbol, title, optional subtitle, and whatever
// you want on the trailing edge.
//
// The separator is inset to line up under the title rather than the icon,
// which is what iOS does and what makes a stack of rows read as a list instead
// of a pile of boxes.
//
// Usage:
// SKRow(title: "Wi-Fi", subtitle: "Home", systemImage: "wifi") { Text("On") }
// SKRow(title: "Account", systemImage: "person.fill", onTap: { })
// SKRow(title: "Last", showsSeparator: false)
//
import SwiftUI
public struct SKRow<Trailing: View>: View {
private let title: String
private let subtitle: String?
private let systemImage: String?
private let tint: Color
private let showsSeparator: Bool
private let onTap: (() -> Void)?
private let trailing: Trailing
public init(
title: String,
subtitle: String? = nil,
systemImage: String? = nil,
tint: Color = SK.Palette.accent,
showsSeparator: Bool = true,
onTap: (() -> Void)? = nil,
@ViewBuilder trailing: () -> Trailing
) {
self.title = title
self.subtitle = subtitle
self.systemImage = systemImage
self.tint = tint
self.showsSeparator = showsSeparator
self.onTap = onTap
self.trailing = trailing()
}
public var body: some View {
if let onTap {
Button(action: onTap) { row }
.buttonStyle(.skPress(scale: 0.99, haptic: .light))
} else {
row
}
}
private var row: some View {
VStack(spacing: 0) {
HStack(spacing: SK.Space.md) {
if let systemImage {
Image(systemName: systemImage)
.font(SK.Icon.symbol(SK.Icon.md))
.foregroundStyle(tint)
.frame(width: SK.Icon.lg)
}
VStack(alignment: .leading, spacing: 1) {
Text(title)
.font(SK.Type_.body)
.foregroundStyle(SK.Palette.label)
if let subtitle {
Text(subtitle)
.font(SK.Type_.caption)
.foregroundStyle(SK.Palette.labelSecondary)
}
}
Spacer(minLength: SK.Space.sm)
trailing
}
.padding(.horizontal, SK.Space.lg)
.padding(.vertical, SK.Space.md)
.frame(minHeight: SK.Icon.hitTarget)
if showsSeparator {
Rectangle()
.fill(SK.Palette.separator)
.frame(height: SK.Stroke.hairline)
// Inset to the title, not the icon — the detail that makes
// a stack of rows read as one list.
.padding(.leading, systemImage == nil
? SK.Space.lg
: SK.Space.lg + SK.Icon.lg + SK.Space.md)
}
}
.contentShape(.rect)
}
}
extension SKRow where Trailing == EmptyView {
/// Row with no trailing content.
public init(
title: String,
subtitle: String? = nil,
systemImage: String? = nil,
tint: Color = SK.Palette.accent,
showsSeparator: Bool = true,
onTap: (() -> Void)? = nil
) {
self.init(
title: title, subtitle: subtitle, systemImage: systemImage,
tint: tint, showsSeparator: showsSeparator, onTap: onTap
) { EmptyView() }
}
}
/// The usual trailing affordance for a row that pushes a screen.
public struct SKChevron: View {
public init() {}
public var body: some View {
Image(systemName: "chevron.right")
.font(SK.Icon.symbol(SK.Icon.sm))
.foregroundStyle(SK.Palette.labelTertiary)
}
}
#Preview("SKRow") {
VStack(spacing: 0) {
SKRow(title: "Wi-Fi", subtitle: "HomeNetwork", systemImage: "wifi") {
Text("On").font(SK.Type_.callout).foregroundStyle(SK.Palette.labelSecondary)
}
SKRow(title: "Account", systemImage: "person.fill", onTap: {}) { SKChevron() }
SKRow(title: "Storage", systemImage: "internaldrive.fill") {
Text("42 GB").font(SK.Type_.callout).foregroundStyle(SK.Palette.labelSecondary)
}
SKRow(title: "Sign Out", systemImage: "arrow.right.square",
tint: SK.Palette.danger, showsSeparator: false, onTap: {})
}
.skSurface(.bordered, radius: SK.Radius.lg)
.padding(SK.Space.lg)
.frame(maxHeight: .infinity)
.background(SK.Palette.surfaceSunken)
}
Questions
- Is this SwiftUI row free for commercial apps?
- Yes. Swift Kit UI is MIT licensed, so you can use it in personal and commercial apps with no payment or sign-up. Keep the licence notice with the code.
- What does it depend on?
- SKRow.swift depends only on SwiftKitUI.swift, a single foundation file with the design tokens and motion curves. There are no third-party packages.
- Which iOS versions does it support?
- iOS 17 and later, plus macOS 14.
Related components
Building a whole app? The Swift Kit adds auth, payments and onboarding, ready to ship.