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
Listrow 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. UseAsyncImageandViewBuilderif 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
LazyVGridwithGridItem(.adaptive(minimum: 100)). Each cell shows an icon and text from a model. Add.refreshableto 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
NavigationStackthat uses aNavigationPathfor 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
ViewModifierthat shows a loading spinner, an error message with retry button, or content based on an enumLoadingState<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
.sheetpresentation 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
ObservableObjecttheme manager that storesprimaryColor,secondaryColor, andfontSize. Inject it via.environmentObjectand use it in a child view that changes the theme when a button is tapped. Use@MainActorfor 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
CachedAsyncImageview that downloads an image from a URL, caches it inNSCache, 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
UIViewthat adds a methodpinToSuperview(insets: UIEdgeInsets)using Auto Layout. It should activate constraints that pin all four edges to the superview. UsetranslatesAutoresizingMaskIntoConstraints = 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
UICollectionViewControllerthat usesUICollectionViewDiffableDataSourceandUICollectionViewCompositionalLayout. 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.keyboardWillShowNotificationandkeyboardWillHideNotification. 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
@IBDesignablesubclass ofUIViewcalledRoundedShadowViewwith@IBInspectableproperties:cornerRadius,shadowColor,shadowOpacity,shadowRadius,shadowOffset. OverridelayoutSubviewsto 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 aUILabelwith a background color. Support a parameter for alignment (leadingorcenter). UseUIStackView.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
PersistenceControllersingleton that creates anNSPersistentContainerfor production and returns an in‑memory container for SwiftUI previews. Include aviewContextcomputed property. UseNSPersistentCloudKitContainerif 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
@FetchRequestto fetchTaskentities whereisCompleted == false, sorted byprioritydescending and then bydueDateascending. TheTaskentity 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
performfor 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
AuditLogentries older than 30 days usingNSBatchDeleteRequest. 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
Decodablemodel. Add error handling and receive on the main thread. UsedataTaskPublisheranddecode(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@Publishedfor a search text string. Subscribe to$searchTextwith.debounce(for: 0.5, scheduler: RunLoop.main)and call a search function that assigns results back to@Publishedarray.
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!
Comments