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

In 2026, AI-assisted development has shifted from a nice-to-have to a daily necessity. As an iOS developer, you can save hours every week by learning how to craft the right prompt. This guide collects 12 battle-tested prompts for working with SwiftUI, UIKit, Core Data, and Combine. Each prompt is presented as a ready-to-use phrase, followed by a real-world usage scenario and a code snippet to make the result concrete. Whether you're building a new app or maintaining a legacy codebase, these prompts will help you get instant, relevant help from ChatGPT, Claude, or any coding assistant.

Why prompts matter for iOS development

Apple's frameworks have a steep learning curve, and even experienced developers face daily challenges with layout, state management, or data persistence. A well-written prompt does more than ask a question—it gives the AI a clear context, defines the required output format, and often includes constraints that prevent hallucinated APIs. According to Apple's official documentation on SwiftUI and UIKit, many APIs are similar but have subtle differences in naming and behavior. A precise prompt reduces back-and-forth and produces code that compiles and runs correctly. Below are 12 prompts that cover the most common tasks.

12 practical prompts for iOS developers

1. SwiftUI: Generate a custom view with state management

Prompt: "Create a simple SwiftUI view for a todo list. Use @State for the array of items and @AppStorage for persisting the last selected filter. Include a TextField to add new items and a Picker to filter by status. Make the code production-ready with proper error handling."

How to use it: This prompt gives the AI a clear scope—view, state, persistence—and asks for production-ready code. In practice, you might use it when starting a new feature and want a working skeleton to modify.

Example: You're building a habit tracker and need a quick list interface. The generated view includes persistent storage via UserDefaults, which is ideal for small local data.

Code:

struct TodoItem: Identifiable, Codable {
    let id = UUID()
    var title: String
    var isDone: Bool
}

enum Filter: String, CaseIterable {
    case all, active, done
}

struct TodoListView: View {
    @State private var items: [TodoItem] = [
        TodoItem(title: "Write blog post", isDone: false),
        TodoItem(title: "Review PR", isDone: true)
    ]
    @State private var newItemTitle = ""
    @AppStorage("selectedFilter") private var selectedFilterRaw = Filter.all.rawValue

    private var filteredItems: [TodoItem] {
        switch Filter(rawValue: selectedFilterRaw) ?? .all {
        case .all: return items
        case .active: return items.filter { !$0.isDone }
        case .done: return items.filter { $0.isDone }
        }
    }

    var body: some View {
        NavigationStack {
            VStack {
                Picker("Filter", selection: $selectedFilterRaw) {
                    ForEach(Filter.allCases, id: \.rawValue) { filter in
                        Text(filter.rawValue.capitalized).tag(filter.rawValue)
                    }
                }
                .pickerStyle(.segmented)
                .padding()

                List {
                    ForEach(filteredItems) { item in
                        HStack {
                            Image(systemName: item.isDone ? "checkmark.circle.fill" : "circle")
                            Text(item.title).strikethrough(item.isDone)
                        }
                        .onTapGesture {
                            if let index = items.firstIndex(where: { $0.id == item.id }) {
                                items[index].isDone.toggle()
                            }
                        }
                    }
                    .onDelete { offsets in
                        items.remove(atOffsets: offsets)
                    }
                }

                HStack {
                    TextField("Add new item", text: $newItemTitle)
                        .textFieldStyle(.roundedBorder)
                    Button("Add") {
                        let trimmed = newItemTitle.trimmingCharacters(in: .whitespaces)
                        guard !trimmed.isEmpty else { return }
                        items.append(TodoItem(title: trimmed, isDone: false))
                        newItemTitle = ""
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(newItemTitle.trimmingCharacters(in: .whitespaces).isEmpty)
                }
                .padding()
            }
            .navigationTitle("Todo List")
        }
    }
}

2. SwiftUI: Convert a UIViewController lifecycle to SwiftUI

Prompt: "Convert the following UIViewController code to a SwiftUI view. The original uses viewDidLoad to set up a UITableView, viewWillAppear to reload data, and a UIRefreshControl. Use @State and List to achieve the same behavior. Keep the existing networking logic."

How to use it: When migrating an existing UIKit app to SwiftUI, this prompt helps you preserve the logic while leveraging SwiftUI's declarative syntax.

Example: You have an old ViewController that shows a list of articles with pull-to-refresh. This prompt produces a SwiftUI List with .refreshable and an @ObservableObject or @State for the data.

Code:

struct ArticleListView: View {
    @State private var articles: [Article] = []
    @State private var isLoading = false

    var body: some View {
        NavigationStack {
            List(articles) { article in
                VStack(alignment: .leading) {
                    Text(article.title).font(.headline)
                    Text(article.summary).font(.caption)
                }
            }
            .refreshable {
                await loadArticles()
            }
            .task {
                await loadArticles()
            }
        }
    }

    private func loadArticles() async {
        isLoading = true
        defer { isLoading = false }
        let url = URL(string: "https://api.example.com/articles")!
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            articles = try decoder.decode([Article].self, from: data)
        } catch {
            // handle error, e.g., show an alert
        }
    }
}

struct Article: Identifiable, Codable {
    let id: Int
    let title: String
    let summary: String
}

3. SwiftUI: Use @EnvironmentObject for dependency injection

Prompt: "Define a SettingsStore class that conforms to ObservableObject. It should have @Published properties for theme, fontSize, and isDarkMode. Use @EnvironmentObject to inject it into the root view and access it in a child view. Include a settings row that toggles dark mode."

How to use it: This prompt is perfect when you need app-wide settings and want to avoid passing data through every initializer.

Example: You're building a reading app; the reader should respect the user's theme and font size. With @EnvironmentObject, any view can read and update the settings without complex delegation.

Code:

final class SettingsStore: ObservableObject {
    @Published var fontSize: CGFloat = 14
    @Published var isDarkMode = false
    @Published var theme = "system"
}

struct SettingsView: View {
    @EnvironmentObject var settings: SettingsStore

    var body: some View {
        Form {
            Toggle("Dark Mode", isOn: $settings.isDarkMode)
            Stepper(value: $settings.fontSize, in: 10...24) {
                Text("Font Size: \(Int(settings.fontSize))")
            }
            Picker("Theme", selection: $settings.theme) {
                Text("System").tag("system")
                Text("Light").tag("light")
                Text("Dark").tag("dark")
            }
        }
        .preferredColorScheme(settings.isDarkMode ? .dark : .light)
    }
}

4. SwiftUI: Create an animation with matchedGeometryEffect

Prompt: "Explain matchedGeometryEffect in SwiftUI and give a real example where a custom shape moves from one position to another. The code should be minimal and runnable."

How to use it: Use this prompt to learn or refresh a tricky animation API. The AI will give a concise explanation and a working demo.

Example: You want to implement a buy button that expands into a confirmation card. matchedGeometryEffect smoothly transitions the button's shape.

Code:

struct MatchedGeometryExample: View {
    @State private var isExpanded = false
    @Namespace private var animation

    var body: some View {
        VStack {
            if isExpanded {
                RoundedRectangle(cornerRadius: 16)
                    .fill(Color.blue)
                    .matchedGeometryEffect(id: "shape", in: animation)
                    .frame(width: 300, height: 150)
                    .overlay(Text("Confirm"))
            } else {
                Circle()
                    .fill(Color.red)
                    .matchedGeometryEffect(id: "shape", in: animation)
                    .frame(width: 60, height: 60)
            }
        }
        .onTapGesture {
            withAnimation(.spring(response: 0.6, dampingFraction: 0.7)) {
                isExpanded.toggle()
            }
        }
    }
}

5. UIKit: Auto Layout constraints programmatically

Prompt: "Write a UIKit view controller that places a label and a button horizontally centered. Use only Auto Layout constraints (no storyboard). The button should trigger a counter increment in the label. Add a bottom constraint that adapts to the safe area."

How to use it: This prompt generates code that works in a storyboard-less environment, which is common for modularized projects.

Example: You're building a small prototype and want to avoid storyboards. This prompt gives you a self-contained layout with proper safe-area handling.

Code:

import UIKit

final class CounterViewController: UIViewController {
    private let counterLabel = UILabel()
    private let incrementButton = UIButton(type: .system)
    private var counter = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        setupConstraints()
    }

    private func setupViews() {
        view.backgroundColor = .white
        counterLabel.text = "0"
        counterLabel.textAlignment = .center
        counterLabel.translatesAutoresizingMaskIntoConstraints = false

        incrementButton.setTitle("Increment", for: .normal)
        incrementButton.addTarget(self, action: #selector(incrementCounter), for: .touchUpInside)
        incrementButton.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(counterLabel)
        view.addSubview(incrementButton)
    }

    private func setupConstraints() {
        NSLayoutConstraint.activate([
            counterLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
            counterLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            counterLabel.widthAnchor.constraint(equalToConstant: 200),

            incrementButton.topAnchor.constraint(equalTo: counterLabel.bottomAnchor, constant: 20),
            incrementButton.centerXAnchor.constraint(equalTo: counterLabel.centerXAnchor),
            incrementButton.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20)
        ])
    }

    @objc private func incrementCounter() {
        counter += 1
        counterLabel.text = "\(counter)"
    }
}

6. UIKit: Implement a custom delegate pattern

Prompt: "Create a custom protocol in Swift for a payment flow. The protocol should have methods for startPayment, cancelPayment, and deliverReceipt. Then implement a PaymentHandler class that uses this delegate to notify the caller about success or failure. Show how to call it from a UIViewController."

How to use it: Delegation is a fundamental pattern in UIKit. This prompt helps you produce clean, maintainable code without creating tight coupling.

Example: You're integrating a payment SDK and want to abstract it behind a protocol. This makes your main class testable with a mock delegate.

Code:

protocol PaymentDelegate: AnyObject {
    func paymentDidSucceed(receipt: String)
    func paymentDidFail(error: Error)
}

final class PaymentHandler {
    weak var delegate: PaymentDelegate?

    func startPayment(amount: Double) {
        // Simulate network call
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            let success = true
            if success {
                let receipt = "PAY-\(UUID().uuidString)"
                self.delegate?.paymentDidSucceed(receipt: receipt)
            } else {
                let error = NSError(domain: "PaymentError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Insufficient funds"])
                self.delegate?.paymentDidFail(error: error)
            }
        }
    }
}

final class CheckoutViewController: UIViewController, PaymentDelegate {
    private lazy var handler = PaymentHandler()

    override func viewDidLoad() {
        super.viewDidLoad()
        handler.delegate = self
    }

    @objc private func payTapped() {
        handler.startPayment(amount: 29.99)
    }

    func paymentDidSucceed(receipt: String) {
        print("Receipt: \(receipt)")
        // Show confirmation alert
    }

    func paymentDidFail(error: Error) {
        print("Error: \(error.localizedDescription)")
        // Show error alert
    }
}

7. UIKit: TableView with diffable data source

Prompt: "Implement a modern UICollectionView or UITableView using UICollectionViewDiffableDataSource. Use a simple model of Song with id, title, artist. The code should update the snapshot when new items are added. Also handle cell registration with UICollectionView.CellRegistration."

How to use it: Diffable data sources simplify updates and animations. This prompt is ideal when you need a self-updating list.

Example: You're building a music player UI where the playlist changes based on user actions. Diffable data source ensures the interface stays in sync with minimal code.

Code:

import UIKit

struct Song: Hashable {
    let id: UUID
    let title: String
    let artist: String
}

final class SongsViewController: UIViewController {
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Song>!

    enum Section {
        case main
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        configureCollectionView()
        configureDataSource()
        applyInitialSnapshot()
    }

    private func configureCollectionView() {
        let layout = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewCompositionalLayout.list(using: layout))
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(collectionView)
    }

    private func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Song> { cell, indexPath, item in
            var content = cell.defaultContentConfiguration()
            content.text = item.title
            content.secondaryText = item.artist
            cell.contentConfiguration = content
            cell.accessories = [.disclosureIndicator()]
        }

        dataSource = UICollectionViewDiffableDataSource<Section, Song>(collectionView: collectionView) { collectionView, indexPath, item in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
        }
    }

    private func applyInitialSnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Song>()
        snapshot.appendSections([.main])
        snapshot.appendItems([Song(id: UUID(), title: "Song 1", artist: "Artist 1")], toSection: .main)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

8. Core Data: Set up the stack with CloudKit sync

Prompt: "Show how to create a modern Core Data stack in Swift with NSPersistentCloudKitContainer. Include the code for loading persistent stores, handling errors, and setting up the view context. Also mention the needed entitlements in Xcode."

How to use it: This prompt is useful when you start a new project and want CloudKit sync from the beginning, as recommended by Apple for cross-device data.

Example: You're building a note-taking app. With NSPersistentCloudKitContainer, you can sync across the user's devices with minimal extra effort.

Code:

import CoreData

class PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentCloudKitContainer

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "YourModelName")

        guard let description = container.persistentStoreDescriptions.first else {
            fatalError("Failed to retrieve persistent store description")
        }

        if inMemory {
            description.url = URL(fileURLWithPath: "/dev/null")
        }

        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

        container.loadPersistentStores { _, error in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        }

        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
}

To enable CloudKit, add the iCloud capability and set the Background Modes checkbox for Remote notifications in Xcode. Also, the model must have an NSPersistentCloudKitContainer setup with history tracking enabled.

9. Core Data: Write a fetch request with sort and filter

Prompt: "Write a Swift function that fetches Task entities from Core Data. The fetch request should be sorted by dueDate ascending, filtered by isCompleted == false, and limited to 10 results. Use @FetchRequest in SwiftUI and also a manual NSFetchRequest in UIKit. Provide the predicate format correctly."

How to use it: This prompt covers both SwiftUI and UIKit ways to fetch with a complex predicate. It's practical for any app using Core Data.

Example: You need to display upcoming incomplete tasks on a dashboard. The predicate filters and sorts the data, and the limit prevents performance issues with large datasets.

Code (SwiftUI):

@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \.dueDate, ascending: true)],
    predicate: NSPredicate(format: "isCompleted == %@", NSNumber(value: false)),
    limit: 10
) private var tasks: FetchedResults<Task>

List(tasks) { task in
    Text(task.title ?? "")
}

Code (UIKit):

let request: NSFetchRequest<Task> = Task.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "dueDate", ascending: true)]
request.predicate = NSPredicate(format: "isCompleted == %@", NSNumber(value: false))
request.fetchLimit = 10

do {
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    let tasks = try context.fetch(request)
    // Use the tasks array
} catch {
    print("Failed to fetch tasks: \(error)")
}

10. Core Data: Create and manage a background context

Prompt: "Show the correct way to perform a Core Data operation on a background context to avoid blocking the main thread. Include code for importing a large JSON batch, saving the context, and merging changes to the main context. Use performBackgroundTask from NSPersistentContainer."

How to use it: This is crucial for any app that ingests data from the network. Improper context usage causes UI freezes and crashes.

Example: You're syncing thousands of records from a REST API. Using a background context keeps the UI responsive.

Code:

let container = PersistenceController.shared.container
container.performBackgroundTask { context in
    let decoder = JSONDecoder()
    decoder.userInfo[.managedObjectContext] = context

    do {
        let items = try decoder.decode([RemoteItem].self, from: jsonData)
        for item in items {
            let entity = ItemEntity(context: context)
            entity.name = item.name
            entity.date = item.date
        }
        try context.save()

        // Merge to main context automatically happens via automaticallyMergesChangesFromParent
    } catch {
        print("Background import error: \(error)")
    }
}

Note: performBackgroundTask creates a private queue context and saves it. The main context, if automaticallyMergesChangesFromParent is true, picks up the changes automatically.

11. Combine: Use Publishers to observe and debounce text field

Prompt: "In SwiftUI, create a TextField that uses Combine to debounce user input for 300 ms and perform a search request. The code should use @Published and Combine's debounce operator, with sink to trigger the network call. Cancel the subscription appropriately."

How to use it: This prompt turns a basic TextField into a live search that doesn't spam the backend. It's a common interview question and a real-world necessity.

Example: A library app with a search bar. Instead of making a request on every keystroke, you wait 300 ms after the user stops typing.

Code:

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

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

    private func performSearch(with text: String) {
        guard !text.isEmpty else {
            results = []
            return
        }
        // Mock network call
        results = ["Result for \(text) 1", "Result for \(text) 2"]
    }
}

struct SearchView: View {
    @StateObject private var viewModel = SearchViewModel()

    var body: some View {
        List(viewModel.results, id: \.self) { result in
            Text(result)
        }
        .searchable(text: $viewModel.query)
    }
}

12. Combine: Combine with SwiftUI for networking

Prompt: "Write a SwiftUI view that fetches a list of GitHub repos from an API using URLSession.shared.dataTaskPublisher and Combine. Handle loading, success, and error states. Use @State, @Published, and URLSession in the view model."

How to use it: Combine publishers work beautifully with SwiftUI. This prompt gives a clean architecture for network calls without external dependencies.

Example: You're creating a developer tool that shows repositories. This pattern can be reused for any GET request.

Code:

struct Repo: Codable, Identifiable {
    let id: Int
    let name: String
    let description: String?
}

final class RepoViewModel: ObservableObject {
    @Published var repos: [Repo] = []
    @Published var errorMessage: String?
    @Published var isLoading = false
    private var cancellables = Set<AnyCancellable>()

    func loadRepos(for user: String) {
        guard let url = URL(string: "https://api.github.com/users/\(user)/repos") else { return }

        isLoading = true
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [Repo].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink { [weak self] completion in
                self?.isLoading = false
                if case .failure(let error) = completion {
                    self?.errorMessage = error.localizedDescription
                }
            } receiveValue: { [weak self] repos in
                self?.repos = repos
            }
            .store(in: &cancellables)
    }
}

struct RepoListView: View {
    @StateObject private var viewModel = RepoViewModel()

    var body: some View {
        Group {
            if viewModel.isLoading {
                ProgressView("Loading")
            } else if let error = viewModel.errorMessage {
                VStack {
                    Text("Error: \(error)")
                    Button("Retry", action: { viewModel.loadRepos(for: "apple") })
                }
            } else {
                List(viewModel.repos) { repo in
                    VStack(alignment: .leading) {
                        Text(repo.name).font(.headline)
                        if let description = repo.description {
                            Text(description).font(.subheadline).foregroundColor(.secondary)
                        }
                    }
                }
            }
        }
        .task {
            viewModel.loadRepos(for: "apple")
        }
    }
}

Prompt engineering tips for iOS

When using prompts, always include the target framework and a concrete task. Add Write production-ready code to avoid commented-out samples. If the AI returns an outdated API, reply with This API was deprecated in iOS 17. Suggest the modern replacement. Don't forget to ask for import statements and Info.plist changes when needed.

For Core Data, always specify the model name and context type. For SwiftUI, indicate whether you use @StateObject or @ObservedObject. For UIKit, specify whether you use storyboards or programmatic layout. This context dramatically improves the quality of the answer.

Final thoughts

These 12 prompts cover the everyday tasks of an iOS developer. By mastering prompt engineering, you not only get faster code generation but also learn subtle API details from the AI's explanations. Start by copying a prompt, adapt it to your project, and soon you'll develop your own library of effective prompts. The key is to treat the AI as a junior developer who needs clear context—the more precise you are, the better the output. Add your own prompts to this list and share them with the community.

Now, go copy the prompt that saves your next hour. Happy coding!

← All posts

Comments