18 Prompts for Swift and iOS Development: SwiftUI, UIKit, Core Data

Introduction

As an iOS developer, you likely use AI tools daily to speed up debugging, generate boilerplate code, or refactor existing logic. But the quality of the output depends entirely on how you phrase your request. A well-crafted prompt can save hours; a vague one can produce unusable results.

This collection brings together 18 battle-tested prompts I actually use in my own workflow. Each prompt targets a specific scenario in SwiftUI, UIKit, Core Data, or Combine. They are designed to be copied, pasted, and adapted — no fluff, just working examples that you can drop into your Xcode project today.

SwiftUI Prompts (1–7)

1. Generate a reusable list row with swipe actions

Prompt:

Write a SwiftUI List row that displays a title, subtitle, and an image from an async URL. Add a trailing swipe action to delete and a leading swipe action to toggle a favorite state. Use AsyncImage and ViewBuilder if possible.

Example usage:

struct ItemRow: View {
    let item: Item
    @Binding var isFavorite: Bool
    var onDelete: () -> Void

    var body: some View {
        HStack {
            AsyncImage(url: URL(string: item.imageURL)) { image in
                image.resizable()
            } placeholder: {
                Color.gray
            }
            .frame(width: 50, height: 50)
            .clipShape(RoundedRectangle(cornerRadius: 8))

            VStack(alignment: .leading) {
                Text(item.title).font(.headline)
                Text(item.subtitle).font(.subheadline).foregroundColor(.secondary)
            }
            .padding(.leading, 8)
        }
        .swipeActions(edge: .leading) {
            Button { isFavorite.toggle() } label: {
                Label("Favorite", systemImage: isFavorite ? "star.fill" : "star")
            }
            .tint(.yellow)
        }
        .swipeActions(edge: .trailing) {
            Button(role: .destructive) { onDelete() } label: {
                Label("Delete", systemImage: "trash")
            }
        }
    }
}

Why it works: The prompt specifies view hierarchy, async image, and swipe actions — all common UI patterns. It produces production-ready code that respects SwiftUI best practices (e.g., @Binding, ViewBuilder).

2. LazyVGrid with dynamic columns and pull-to-refresh

Prompt:

Build a SwiftUI grid view that adapts to screen size using LazyVGrid with GridItem(.adaptive(minimum: 100)). Each cell shows an icon and text from a model. Add .refreshable to reload data from an async function.

Example usage:

struct AdaptiveGrid: View {
    @State private var items: [GridModel] = []

    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 16) {
                ForEach(items) { item in
                    VStack {
                        Image(systemName: item.iconName)
                            .font(.largeTitle)
                        Text(item.label)
                            .font(.caption)
                    }
                    .padding()
                    .background(Color.gray.opacity(0.1))
                    .cornerRadius(12)
                }
            }
            .padding()
        }
        .refreshable {
            items = await loadGridData()
        }
        .task { items = await loadGridData() }
    }

    func loadGridData() async -> [GridModel] { /* network call */ [] }
}

Why it works: The prompt asks for adaptive columns and pull-to-refresh — two features that often require careful handling of state and async. The response correctly places .task for initial load and keeps the view stateless outside of @State.

3. Custom navigation with path and deep link handling

Prompt:

Create a SwiftUI NavigationStack that uses a NavigationPath for programmatic navigation. Include a method to handle incoming deep links (e.g., myapp://detail/42) that pushes a detail view with the matching ID.

Example usage:

struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(1...10, id: \.self) { id in
                NavigationLink(value: id) {
                    Text("Item \(id)")
                }
            }
            .navigationDestination(for: Int.self) { id in
                DetailView(id: id)
            }
        }
        .onOpenURL { url in handleDeepLink(url) }
    }

    private func handleDeepLink(_ url: URL) {
        guard url.scheme == "myapp",
              url.host == "detail",
              let id = Int(url.pathComponents.last ?? "") else { return }
        path.append(id)
    }
}

Why it works: Deep linking in SwiftUI is error‑prone; the prompt explicitly asks for NavigationPath and onOpenURL, which is the correct approach for iOS 16+. The code handles parsing and state mutation safely.

4. Build a reusable LoadingState view modifier

Prompt:

Write a SwiftUI ViewModifier that shows a loading spinner, an error message with retry button, or content based on an enum LoadingState<T>. Use it as a .modifier(loadingState: ...) on any view.

Example usage:

enum LoadingState<T> {
    case idle, loading, success(T), failure(Error)
}

struct LoadingModifier<T>: ViewModifier where T: View {
    let state: LoadingState<T>
    let retry: () -> Void

    func body(content: Content) -> some View {
        switch state {
        case .idle, .loading:
            ProgressView()
        case .success(let view):
            view
        case .failure:
            VStack {
                Text("Something went wrong")
                Button("Retry", action: retry)
            }
        }
    }
}

extension View {
    func loadingState<T: View>(_ state: LoadingState<T>, retry: @escaping () -> Void) -> some View {
        modifier(LoadingModifier(state: state, retry: retry))
    }
}

Why it works: A common pattern — the prompt yields a generic modifier that doesn’t alter the original view’s layout. Note: the success case holding a View is unusual; usually it holds the data. But the prompt specifically asks for view presence, so it’s correct.

5. Animate a custom transition for sheet presentation

Prompt:

Create a custom transition in SwiftUI that slides a view in from the bottom while fading in the background. Apply it to a .sheet presentation using .transition(.slideFade) and .animation(.spring()).

Example usage:

extension AnyTransition {
    static var slideFade: AnyTransition {
        .asymmetric(
            insertion: .move(edge: .bottom).combined(with: .opacity),
            removal: .move(edge: .bottom).combined(with: .opacity)
        )
    }
}

struct SheetView: View {
    @Binding var isPresented: Bool

    var body: some View {
        VStack {
            Text("Sheet Content")
            Button("Dismiss") { isPresented = false }
        }
        .frame(maxWidth: .infinity, maxHeight: 300)
        .background(.regularMaterial)
        .transition(.slideFade)
        .animation(.spring(), value: isPresented)
    }
}

Why it works: The prompt combines asymmetric transition and spring() animation — the result is a smooth, custom sheet that doesn’t rely on the stock .sheet modifier. The code is succinct and idiomatic.

6. Environment object for theme management

Prompt:

Implement an ObservableObject theme manager that stores primaryColor, secondaryColor, and fontSize. Inject it via .environmentObject and use it in a child view that changes the theme when a button is tapped. Use @MainActor for thread safety.

Example usage:

@MainActor
final class ThemeManager: ObservableObject {
    @Published var primaryColor = Color.blue
    @Published var secondaryColor = Color.gray
    @Published var fontSize: CGFloat = 16
}

struct ContentView: View {
    @EnvironmentObject var theme: ThemeManager

    var body: some View {
        VStack {
            Text("Hello")
                .foregroundColor(theme.primaryColor)
                .font(.system(size: theme.fontSize))
            Button("Switch Theme") {
                theme.primaryColor = theme.primaryColor == .blue ? .red : .blue
            }
        }
        .padding()
    }
}

Why it works: @MainActor prevents data races, and @EnvironmentObject makes it simple to inject the manager. The prompt correctly asks for a button that changes properties in real time.

7. Async image with caching and placeholder

Prompt:

Write a custom CachedAsyncImage view that downloads an image from a URL, caches it in NSCache, and shows a placeholder while loading. If the download fails, show a system image as fallback.

Example usage:

struct CachedAsyncImage: View {
    let url: URL
    static let cache = NSCache<NSURL, UIImage>()
    @State private var image: UIImage?

    var body: some View {
        Group {
            if let image = image {
                Image(uiImage: image).resizable()
            } else {
                Image(systemName: "photo")
                    .foregroundColor(.gray)
            }
        }
        .task {
            if let cached = Self.cache.object(forKey: url as NSURL) {
                image = cached
            } else if let (data, _) = try? await URLSession.shared.data(from: url),
                      let uiImage = UIImage(data: data) {
                Self.cache.setObject(uiImage, forKey: url as NSURL)
                image = uiImage
            }
        }
    }
}

Why it works: The prompt explicitly asks for caching and fallback — many developers assume built‑in AsyncImage handles this, but it doesn’t. This lightweight wrapper gives you control over memory usage.

UIKit Prompts (8–12)

8. Auto‑layout helper for programmatic constraints

Prompt:

Provide a Swift extension on UIView that adds a method pinToSuperview(insets: UIEdgeInsets) using Auto Layout. It should activate constraints that pin all four edges to the superview. Use translatesAutoresizingMaskIntoConstraints = false.

Example usage:

extension UIView {
    func pinToSuperview(insets: UIEdgeInsets = .zero) {
        guard let superview = superview else { return }
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            topAnchor.constraint(equalTo: superview.topAnchor, constant: insets.top),
            leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: insets.left),
            trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: -insets.right),
            bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: -insets.bottom)
        ])
    }
}

Why it works: The prompt eliminates repetitive constraint setup. The code is safe (guards superview) and respects insets.

9. UICollectionView with diffable data source and compositional layout

Prompt:

Create a UICollectionViewController that uses UICollectionViewDiffableDataSource and UICollectionViewCompositionalLayout. The list should have two sections: featured (horizontal scroll) and standard (vertical list). Register cells programmatically.

Example usage:

class MyCollectionVC: UICollectionViewController {
    enum Section { case featured, standard }
    struct Item: Hashable { let id: UUID; let title: String }

    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.collectionViewLayout = createLayout()
        configureDataSource()
    }

    private func createLayout() -> UICollectionViewCompositionalLayout {
        UICollectionViewCompositionalLayout { sectionIndex, _ in
            let section = sectionIndex == 0 ? Section.featured : .standard
            switch section {
            case .featured:
                let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(0.8), heightDimension: .absolute(200)), subitems: [item])
                return NSCollectionLayoutSection(group: group)
            case .standard:
                let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(44)))
                let group = NSCollectionLayoutGroup.vertical(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(44)), subitems: [item])
                return NSCollectionLayoutSection(group: group)
            }
        }
    }

    private func configureDataSource() {
        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
            var config = cell.defaultContentConfiguration()
            config.text = item.title
            cell.contentConfiguration = config
            return cell
        }
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
    }
}

Why it works: Diffable data sources and compositional layouts are the modern UIKit approach. The prompt distinguishes between section types and produces a clean, decoupled configuration.

10. UIViewController with keyboard handling

Prompt:

Write a UIViewController that adjusts its safe area when the keyboard appears. Register for UIResponder.keyboardWillShowNotification and keyboardWillHideNotification. Animate the bottom constraint change with the same curve and duration.

Example usage:

class KeyboardAwareVC: UIViewController {
    @IBOutlet weak var bottomConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    @objc private func keyboardWillShow(_ notification: Notification) {
        guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
              let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
              let curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { return }
        bottomConstraint.constant = frame.height - view.safeAreaInsets.bottom
        UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: curve), animations: self.view.layoutIfNeeded)
    }

    @objc private func keyboardWillHide(_ notification: Notification) {
        bottomConstraint.constant = 0
        UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
    }
}

Why it works: The prompt extracts animation info from the notification — a detail many tutorials miss. The result is smooth movement matching the system keyboard animation.

11. Custom UIView with @IBDesignable and @IBInspectable

Prompt:

Create an @IBDesignable subclass of UIView called RoundedShadowView with @IBInspectable properties: cornerRadius, shadowColor, shadowOpacity, shadowRadius, shadowOffset. Override layoutSubviews to apply layer properties.

Example usage:

@IBDesignable
class RoundedShadowView: UIView {
    @IBInspectable var cornerRadius: CGFloat = 0 { didSet { setNeedsLayout() } }
    @IBInspectable var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
    @IBInspectable var shadowOpacity: Float = 0.5 { didSet { setNeedsLayout() } }
    @IBInspectable var shadowRadius: CGFloat = 5 { didSet { setNeedsLayout() } }
    @IBInspectable var shadowOffset: CGSize = .zero { didSet { setNeedsLayout() } }

    override func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = cornerRadius
        layer.shadowColor = shadowColor.cgColor
        layer.shadowOpacity = shadowOpacity
        layer.shadowRadius = shadowRadius
        layer.shadowOffset = shadowOffset
        layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath
        layer.masksToBounds = false
    }
}

Why it works: @IBDesignable lets you preview in Interface Builder, and @IBInspectable surfaces properties in the attributes inspector. The prompt asks for layoutSubviews override because layer properties must be set after layout.

12. UIStackView with dynamic arranged subviews

Prompt:

Write a helper function that takes an array of strings and returns a UIStackView (vertical, spacing 8). Each string becomes a UILabel with a background color. Support a parameter for alignment (leading or center). Use UIStackView.distribution = .fillProportionally.

Example usage:

func makeTagStack(tags: [String], alignment: UIStackView.Alignment = .leading) -> UIStackView {
    let stack = UIStackView()
    stack.axis = .vertical
    stack.spacing = 8
    stack.alignment = alignment
    stack.distribution = .fillProportionally
    for tag in tags {
        let label = UILabel()
        label.text = tag
        label.backgroundColor = UIColor.systemGray5
        label.textAlignment = .center
        label.layer.cornerRadius = 4
        label.clipsToBounds = true
        stack.addArrangedSubview(label)
    }
    return stack
}

Why it works: The prompt is specific about distribution and alignment. The resulting stack is reusable and configurable.

Core Data Prompts (13–16)

13. Core Data stack with NSPersistentContainer and preview context

Prompt:

Build a PersistenceController singleton that creates an NSPersistentContainer for production and returns an in‑memory container for SwiftUI previews. Include a viewContext computed property. Use NSPersistentCloudKitContainer if available.

Example usage:

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "Model")
        if inMemory {
            container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores { _, error in
            if let error { fatalError("\(error)") }
        }
        container.viewContext.automaticallyMergesChangesFromParent = true
    }

    var viewContext: NSManagedObjectContext { container.viewContext }

    static var preview: PersistenceController {
        let controller = PersistenceController(inMemory: true)
        // insert sample data
        return controller
    }
}

Why it works: The prompt explicitly asks for in‑memory preview and CloudKit container — two practical requirements for modern Core Data apps. The singleton pattern is standard.

14. Fetch request with @FetchRequest and predicate using NSPredicate

Prompt:

In a SwiftUI view, use @FetchRequest to fetch Task entities where isCompleted == false, sorted by priority descending and then by dueDate ascending. The Task entity has attributes: title, priority (Int16), dueDate (Date), isCompleted (Bool).

Example usage:

struct TaskListView: View {
    @FetchRequest(
        sortDescriptors: [
            SortDescriptor(\.priority, order: .reverse),
            SortDescriptor(\.dueDate, order: .forward)
        ],
        predicate: NSPredicate(format: "isCompleted == NO")
    ) var tasks: FetchedResults<Task>

    var body: some View {
        List(tasks) { task in
            Text(task.title ?? "")
        }
    }
}

Why it works: The prompt specifies both sort and filter. The response correctly uses SortDescriptor (iOS 15+) and NSPredicate.

15. Background context for saving without blocking UI

Prompt:

Write a Core Data save function that creates a private background context, performs changes, and merges them back to the main context. Handle errors by logging and rolling back. Use perform for thread safety.

Example usage:

extension PersistenceController {
    func saveInBackground(_ block: @escaping (NSManagedObjectContext) -> Void) {
        let bgContext = container.newBackgroundContext()
        bgContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        bgContext.perform {
            block(bgContext)
            if bgContext.hasChanges {
                do {
                    try bgContext.save()
                } catch {
                    print("Failed to save: \(error)")
                    bgContext.rollback()
                }
            }
        }
    }
}

Why it works: The prompt asks for background context and merge policy — essential for performance. The code ensures changes are written without blocking the main thread.

16. Batch delete with fetch request

Prompt:

Implement a method that deletes all AuditLog entries older than 30 days using NSBatchDeleteRequest. Run it on a private context and refresh the main context after deletion.

Example usage:

func deleteOldLogs() {
    let context = container.newBackgroundContext()
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "AuditLog")
    let cutoff = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
    fetchRequest.predicate = NSPredicate(format: "timestamp < %@", cutoff as NSDate)
    let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)
    batchDelete.resultType = .resultTypeObjectIDs

    context.perform {
        do {
            let result = try context.execute(batchDelete) as? NSBatchDeleteResult
            if let objectIDs = result?.result as? [NSManagedObjectID] {
                NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [NSDeletedObjectsKey: objectIDs], into: [viewContext])
            }
        } catch {
            print("Batch delete failed: \(error)")
        }
    }
}

Why it works: Batch deletes are efficient but require manual merge. The prompt correctly asks for result IDs and merge. This pattern comes directly from Apple’s Core Data documentation.

Combine Prompts (17–18)

17. Simple networking layer with URLSession.DataTaskPublisher

Prompt:

Write a Combine publisher that fetches JSON from a URL and decodes it into a Decodable model. Add error handling and receive on the main thread. Use dataTaskPublisher and decode(type:decoder:).

Example usage:

struct APIService {
    static func fetch<T: Decodable>(_ type: T.Type, from url: URL) -> AnyPublisher<T, Error> {
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: T.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

Why it works: The prompt chains the standard Combine operators. The .receive(on:) ensures UI updates on the main thread. The generic T makes it reusable.

18. Combine @Published property with debounce for search

Prompt:

In a SwiftUI view model (ObservableObject), use @Published for a search text string. Subscribe to $searchText with .debounce(for: 0.5, scheduler: RunLoop.main) and call a search function that assigns results back to @Published array.

Example usage:

final class SearchViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var results: [String] = []
    private var cancellables: Set<AnyCancellable> = []

    init() {
        $searchText
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .removeDuplicates()
            .sink { [weak self] text in
                self?.performSearch(query: text)
            }
            .store(in: &cancellables)
    }

    private func performSearch(query: String) {
        // mock async search
        results = ["Result for \(query)"]
    }
}

Why it works: Debounce prevents excessive search calls. The removeDuplicates avoids duplicate identical queries. This is the standard approach for live search in SwiftUI.

Conclusion

These 18 prompts reflect patterns I use daily — from SwiftUI navigation and Core Data batch deletes to Combine debounce and UIKit keyboard handling. The key is to be precise: specify the framework, the desired behavior, and any edge cases (like caching or thread safety).

Try copying one of these prompts into your next AI assistant session and adapt the code to your own project. You’ll likely produce production‑ready code in seconds instead of minutes. And remember: the best prompts are the ones you refine over time. Keep a personal library of prompts that work for you.

Happy coding!

← All posts

Comments