mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
New plugin covering mobile (iOS, Android, React Native) and web applications with modern design patterns, accessibility, and design systems. Components: - 9 skills: design-system-patterns, accessibility-compliance, responsive-design, mobile-ios-design, mobile-android-design, react-native-design, web-component-design, interaction-design, visual-design-foundations - 4 commands: design-review, create-component, accessibility-audit, design-system-setup - 3 agents: ui-designer, accessibility-expert, design-system-architect Marketplace updated: - Version bumped to 1.3.4 - 102 agents (+3), 116 skills (+9)
14 KiB
14 KiB
SwiftUI Component Library
Lists and Collections
Basic List
struct ItemListView: View {
@State private var items: [Item] = []
var body: some View {
List {
ForEach(items) { item in
ItemRow(item: item)
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItems)
}
.listStyle(.insetGrouped)
.refreshable {
await loadItems()
}
}
private func deleteItems(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
}
private func moveItems(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
}
}
Sectioned List
struct SectionedListView: View {
let groupedItems: [String: [Item]]
var body: some View {
List {
ForEach(groupedItems.keys.sorted(), id: \.self) { key in
Section(header: Text(key)) {
ForEach(groupedItems[key] ?? []) { item in
ItemRow(item: item)
}
}
}
}
.listStyle(.sidebar)
}
}
Search Integration
struct SearchableListView: View {
@State private var searchText = ""
@State private var items: [Item] = []
var filteredItems: [Item] {
if searchText.isEmpty {
return items
}
return items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
var body: some View {
NavigationStack {
List(filteredItems) { item in
ItemRow(item: item)
}
.searchable(text: $searchText, prompt: "Search items")
.searchSuggestions {
ForEach(searchSuggestions, id: \.self) { suggestion in
Text(suggestion)
.searchCompletion(suggestion)
}
}
.navigationTitle("Items")
}
}
}
Forms and Input
Settings Form
struct SettingsView: View {
@AppStorage("notifications") private var notificationsEnabled = true
@AppStorage("soundEnabled") private var soundEnabled = true
@State private var selectedTheme = Theme.system
@State private var username = ""
var body: some View {
Form {
Section("Account") {
TextField("Username", text: $username)
.textContentType(.username)
.autocorrectionDisabled()
}
Section("Preferences") {
Toggle("Enable Notifications", isOn: $notificationsEnabled)
Toggle("Sound Effects", isOn: $soundEnabled)
Picker("Theme", selection: $selectedTheme) {
ForEach(Theme.allCases) { theme in
Text(theme.rawValue).tag(theme)
}
}
}
Section("About") {
LabeledContent("Version", value: "1.0.0")
Link(destination: URL(string: "https://example.com/privacy")!) {
Text("Privacy Policy")
}
}
}
.navigationTitle("Settings")
}
}
Custom Input Fields
struct ValidatedTextField: View {
let title: String
@Binding var text: String
let validation: (String) -> Bool
@State private var isValid = true
@FocusState private var isFocused: Bool
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.caption)
.foregroundStyle(.secondary)
TextField(title, text: $text)
.textFieldStyle(.roundedBorder)
.focused($isFocused)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(borderColor, lineWidth: 1)
)
.onChange(of: text) { _, newValue in
isValid = validation(newValue)
}
if !isValid && !text.isEmpty {
Text("Invalid input")
.font(.caption)
.foregroundStyle(.red)
}
}
}
private var borderColor: Color {
if isFocused {
return isValid ? .blue : .red
}
return .clear
}
}
Buttons and Actions
Button Styles
// Primary filled button
Button("Continue") {
// action
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
// Secondary button
Button("Cancel") {
// action
}
.buttonStyle(.bordered)
// Destructive button
Button("Delete", role: .destructive) {
// action
}
.buttonStyle(.bordered)
// Custom button style
struct ScaleButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
Menu and Context Menu
// Menu button
Menu {
Button("Edit", systemImage: "pencil") { }
Button("Duplicate", systemImage: "doc.on.doc") { }
Divider()
Button("Delete", systemImage: "trash", role: .destructive) { }
} label: {
Image(systemName: "ellipsis.circle")
}
// Context menu on any view
Text("Long press me")
.contextMenu {
Button("Copy", systemImage: "doc.on.doc") { }
Button("Share", systemImage: "square.and.arrow.up") { }
} preview: {
ItemPreviewView()
}
Sheets and Modals
Sheet Presentation
struct ParentView: View {
@State private var showSettings = false
@State private var selectedItem: Item?
var body: some View {
VStack {
Button("Settings") {
showSettings = true
}
}
.sheet(isPresented: $showSettings) {
SettingsSheet()
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
.sheet(item: $selectedItem) { item in
ItemDetailSheet(item: item)
.presentationDetents([.height(300), .large])
.presentationCornerRadius(24)
}
}
}
struct SettingsSheet: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
SettingsContent()
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
}
}
}
}
}
Confirmation Dialog
struct DeleteConfirmationView: View {
@State private var showConfirmation = false
var body: some View {
Button("Delete Account", role: .destructive) {
showConfirmation = true
}
.confirmationDialog(
"Delete Account",
isPresented: $showConfirmation,
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
deleteAccount()
}
Button("Cancel", role: .cancel) { }
} message: {
Text("This action cannot be undone.")
}
}
}
Loading and Progress
Progress Indicators
// Indeterminate spinner
ProgressView()
.progressViewStyle(.circular)
// Determinate progress
ProgressView(value: downloadProgress, total: 1.0) {
Text("Downloading...")
} currentValueLabel: {
Text("\(Int(downloadProgress * 100))%")
}
// Custom loading view
struct LoadingOverlay: View {
let message: String
var body: some View {
ZStack {
Color.black.opacity(0.4)
.ignoresSafeArea()
VStack(spacing: 16) {
ProgressView()
.scaleEffect(1.5)
.tint(.white)
Text(message)
.font(.subheadline)
.foregroundStyle(.white)
}
.padding(24)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
}
}
}
Skeleton Loading
struct SkeletonRow: View {
@State private var isAnimating = false
var body: some View {
HStack(spacing: 12) {
Circle()
.fill(.gray.opacity(0.3))
.frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 8) {
RoundedRectangle(cornerRadius: 4)
.fill(.gray.opacity(0.3))
.frame(height: 14)
.frame(maxWidth: 200)
RoundedRectangle(cornerRadius: 4)
.fill(.gray.opacity(0.2))
.frame(height: 12)
.frame(maxWidth: 150)
}
}
.opacity(isAnimating ? 0.5 : 1.0)
.animation(.easeInOut(duration: 0.8).repeatForever(), value: isAnimating)
.onAppear { isAnimating = true }
}
}
Async Content Loading
AsyncImage
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
case .failure:
Image(systemName: "photo")
.foregroundStyle(.secondary)
@unknown default:
EmptyView()
}
}
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 8))
Task-Based Loading
struct AsyncContentView: View {
@State private var items: [Item] = []
@State private var isLoading = true
@State private var error: Error?
var body: some View {
Group {
if isLoading {
ProgressView("Loading...")
} else if let error {
ContentUnavailableView(
"Failed to Load",
systemImage: "exclamationmark.triangle",
description: Text(error.localizedDescription)
)
} else if items.isEmpty {
ContentUnavailableView(
"No Items",
systemImage: "tray",
description: Text("Add your first item to get started.")
)
} else {
List(items) { item in
ItemRow(item: item)
}
}
}
.task {
await loadItems()
}
}
private func loadItems() async {
do {
items = try await api.fetchItems()
isLoading = false
} catch {
self.error = error
isLoading = false
}
}
}
Animations
Implicit Animations
struct AnimatedCard: View {
@State private var isExpanded = false
var body: some View {
VStack {
Text("Tap to expand")
if isExpanded {
Text("Additional content here")
.transition(.move(edge: .top).combined(with: .opacity))
}
}
.padding()
.frame(maxWidth: .infinity)
.background(.blue.opacity(0.1), in: RoundedRectangle(cornerRadius: 12))
.onTapGesture {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
isExpanded.toggle()
}
}
}
}
Custom Transitions
extension AnyTransition {
static var slideAndFade: AnyTransition {
.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)
)
}
static var scaleAndFade: AnyTransition {
.scale(scale: 0.8).combined(with: .opacity)
}
}
Phase Animator (iOS 17+)
struct PulsingButton: View {
var body: some View {
Button("Tap Me") { }
.buttonStyle(.borderedProminent)
.phaseAnimator([false, true]) { content, phase in
content
.scaleEffect(phase ? 1.05 : 1.0)
} animation: { _ in
.easeInOut(duration: 0.5)
}
}
}
Gestures
Drag Gesture
struct DraggableCard: View {
@State private var offset = CGSize.zero
@State private var isDragging = false
var body: some View {
RoundedRectangle(cornerRadius: 16)
.fill(.blue)
.frame(width: 200, height: 150)
.offset(offset)
.scaleEffect(isDragging ? 1.05 : 1.0)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
isDragging = true
}
.onEnded { _ in
withAnimation(.spring()) {
offset = .zero
isDragging = false
}
}
)
}
}
Simultaneous Gestures
struct ZoomableImage: View {
@State private var scale: CGFloat = 1.0
@State private var lastScale: CGFloat = 1.0
var body: some View {
Image("photo")
.resizable()
.aspectRatio(contentMode: .fit)
.scaleEffect(scale)
.gesture(
MagnificationGesture()
.onChanged { value in
scale = lastScale * value
}
.onEnded { _ in
lastScale = scale
}
)
.gesture(
TapGesture(count: 2)
.onEnded {
withAnimation {
scale = 1.0
lastScale = 1.0
}
}
)
}
}