Loading...
Loading...
Build SwiftUI layouts using stacks, grids, lists, scroll views, forms, and controls. Covers VStack/HStack/ZStack, LazyVGrid/LazyHGrid, List with sections and swipe actions, ScrollView with ScrollViewReader, Form with validation, Toggle/Picker/Slider, .searchable, and overlay patterns. Use when building data-driven layouts, collection views, settings screens, search interfaces, or transient overlay UI.
npx skill4agent add dpearson2699/swift-ios-skills swiftui-layout-componentsVStackHStackZStackVStack(alignment: .leading, spacing: 8) {
Text(title).font(.headline)
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}LazyVStackLazyHStackScrollViewScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemRow(item: item)
}
}
.padding(.horizontal)
}LazyVGrid.adaptive.flexible// Adaptive grid -- columns adjust to fit
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]
LazyVGrid(columns: columns, spacing: 6) {
ForEach(items) { item in
ThumbnailView(item: item)
.aspectRatio(1, contentMode: .fit)
}
}// Fixed 3-column grid
let columns = Array(repeating: GridItem(.flexible(minimum: 100), spacing: 4), count: 3)
LazyVGrid(columns: columns, spacing: 4) {
ForEach(items) { item in
ThumbnailView(item: item)
}
}.aspectRatioGeometryReader.onGeometryChangereferences/grids.mdListList {
Section("General") {
NavigationLink("Display") { DisplaySettingsView() }
NavigationLink("Haptics") { HapticsSettingsView() }
}
Section("Account") {
Button("Sign Out", role: .destructive) { }
}
}
.listStyle(.insetGrouped).listStyle(.plain).insetGrouped.scrollContentBackground(.hidden).listRowInsets(...).listRowSeparator(.hidden)ScrollViewReader.refreshable { }.contentShape(Rectangle()).scrollEdgeEffectStyle(.soft, for: .top)references/list.mdScrollViewScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 8) {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message).id(message.id)
}
}
}
.onChange(of: messages.last?.id) { _, newValue in
if let id = newValue {
withAnimation { proxy.scrollTo(id, anchor: .bottom) }
}
}
}safeAreaInset(edge:).scrollEdgeEffectStyle(.soft, for: .top).backgroundExtensionEffect().safeAreaBar(edge:)references/scrollview.mdFormSectionForm {
Section("Notifications") {
Toggle("Mentions", isOn: $prefs.mentions)
Toggle("Follows", isOn: $prefs.follows)
}
Section("Appearance") {
Picker("Theme", selection: $theme) {
ForEach(Theme.allCases, id: \.self) { Text($0.title).tag($0) }
}
Slider(value: $fontScale, in: 0.5...1.5, step: 0.1)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)@FocusStateNavigationStack| Control | Usage |
|---|---|
| Boolean preferences |
| Discrete choices; |
| Numeric ranges with visible value label |
| Date/time selection |
| Text input with |
@State@Binding@AppStorageForm.disabled(...)Label// Toggle sections
Form {
Section("Notifications") {
Toggle("Mentions", isOn: $preferences.notificationsMentionsEnabled)
Toggle("Follows", isOn: $preferences.notificationsFollowsEnabled)
}
}
// Slider with value text
Section("Font Size") {
Slider(value: $fontSizeScale, in: 0.5...1.5, step: 0.1)
Text("Scale: \(String(format: "%.1f", fontSizeScale))")
}
// Picker for enums
Picker("Default Visibility", selection: $visibility) {
ForEach(Visibility.allCases, id: \.self) { option in
Text(option.title).tag(option)
}
}.pickerStyle(.segmented)references/form.md.searchable.searchScopes.task(id:)@MainActor
struct ExploreView: View {
@State private var searchQuery = ""
@State private var searchScope: SearchScope = .all
@State private var isSearching = false
@State private var results: [SearchResult] = []
var body: some View {
List {
if isSearching {
ProgressView()
} else {
ForEach(results) { result in
SearchRow(result: result)
}
}
}
.searchable(
text: $searchQuery,
placement: .navigationBarDrawer(displayMode: .always),
prompt: Text("Search")
)
.searchScopes($searchScope) {
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.title)
}
}
.task(id: searchQuery) {
await runSearch()
}
}
private func runSearch() async {
guard !searchQuery.isEmpty else {
results = []
return
}
isSearching = true
defer { isSearching = false }
try? await Task.sleep(for: .milliseconds(250))
results = await fetchResults(query: searchQuery, scope: searchScope)
}
}.overlay(alignment:)struct AppRootView: View {
@State private var toast: Toast?
var body: some View {
content
.overlay(alignment: .top) {
if let toast {
ToastView(toast: toast)
.transition(.move(edge: .top).combined(with: .opacity))
.onAppear {
Task {
try? await Task.sleep(for: .seconds(2))
withAnimation { self.toast = nil }
}
}
}
}
}
}.top.bottom.fullScreenCover(item:)GeometryReaderForEachListScrollViewLazyVStack.contentShape(Rectangle()).presentationSizingListScrollView.pickerStyle(.segmented)LazyVStackLazyHStackIdentifiableForEachGeometryReaderList.plain.insetGroupedForm.searchable.task(id:).refreshable.contentShape(Rectangle())@FocusStatereferences/grids.mdreferences/list.mdreferences/scrollview.mdreferences/form.mdswiftui-patternsswiftui-navigation