Custom alert in SwiftUI

Teresa Bagalà
4 min readJan 30, 2023

--

How to create a custom alert with TextField and custom Buttons in SwiftUI

Photo by Sanjit Pandey on Pixabay

Before starting I would like to recommend reading the book SwiftUI Essentials iOS 16 edition, from which I draw a lot of my knowledge about SwiftUI

This demo app was built with Xcode 14.2 and iOS 16.2. In the following article you can find the app video.

Start Xcode and create a new iOS project called AlertDemo. I want to create a persistent list of items using CoreData . Each new Item is added to the list typing the Item name in a custom alert within a TextField.

The first step is the creation of CoreData model that I call AlertDemoModel:

I create an Entity called Item with an name attribute of String type, that represents the generic item that I will insert in the list.

Now under Model group I create the PersistenceContainer structure with the methods to save and delete an Item:


import Foundation
import CoreData

struct PersistenceContainer {
static let shared = PersistenceContainer()
let container: NSPersistentContainer

init() {
container = NSPersistentContainer(name: "AlertDemoModel")
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores { (_ , error) in
if let error = error as NSError? {
fatalError("Container load failed: \(error)")
}
}
}
func save() throws{
let context = container.viewContext
guard context.hasChanges else { return }
do {
try context.save()
} catch {
print(error.localizedDescription)
}
}

func delete(_ object: NSManagedObject) throws{
let context = container.viewContext
context.delete(object)
try context.save()

}

}

In AlertDemoApp.swift we have :

import SwiftUI
import CoreData

@main
struct AlertDemoApp: App {

let persistenceContainer = PersistenceContainer.shared
var body: some Scene {

WindowGroup {
ContentView()
.environment(\.managedObjectContext,
persistenceContainer.container.viewContext)
}
}
}

Under Views group I create the CustomAlert.swift struct that displays a TextField in which to type the item name. In the custom alert there are the Done and Cancel buttons. If I press Done button and the TextField is empty, a warning message appears indicating that the text must be entered. The two buttons are implemented in structure CustomButton.swift:

import SwiftUI

struct CustomButton: View {
var text : String
var action: () -> Void
var body: some View {
Button(text, action: {
action()
})
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.cyan, lineWidth: 1)
)
.buttonStyle(MyButtonStyle())
}
}

struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 1.3 : 1.0)
}
}

And here the CustomAlert structure:

import Foundation
import SwiftUI

struct CustomAlert: View{
@Binding var textFieldValue: String
@Binding var showAlertWithTextField : Bool
@State var showError: Bool = false
var title: String
var placeholder: String
var handler : () -> Void
var body: some View {
ZStack(alignment: .top) {
Color.white

if showAlertWithTextField{
VStack {
VStack{
Text(title).padding(5)
TextField(placeholder, text: $textFieldValue)
.textFieldStyle(.roundedBorder)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.cyan, lineWidth: 1)
)
.onChange(of: textFieldValue) { newValue in
showError = false
}

Spacer(minLength: 25)
HStack{
CustomButton(text: "Cancel") {
showAlertWithTextField.toggle()
textFieldValue = ""
}

Spacer()

CustomButton(text: "Done"){
if textFieldValue.count > 0{
handler()
textFieldValue = ""
showAlertWithTextField.toggle()
}else{
showError = true

}
}
}
}
}
.padding()


if showError{
HStack{
Image(systemName: "exclamationmark.triangle")
.foregroundStyle(Color.red)
.padding(4)
Text("Insert text")
.padding(4)
.font(.custom("ArialRoundedMTBold", size: 14))
.foregroundColor(Color.red)
}

}
}
}

.frame(width: 300, height: 180)
.cornerRadius(20).shadow(color: .cyan, radius: 8)
.foregroundColor(Color.cyan)

}
}

Note the ZStack root which contains both the alert and the warning that pops up if you try to enter an empty string.

The @Binding properties wrappers are mapped to the corresponding @State properties wrappers in the ContentView. The handler is called only if the TextField is not empty, and matches the save() method in ContentView structure

import SwiftUI
import CoreData

struct ContentView: View {
@State var showTextFieldAlert : Bool = false
@State var showList: Bool = true
@State var textFieldValue: String = ""

@Environment(\.dismiss) var dismiss
@Environment(\.managedObjectContext) private var viewContext

@FetchRequest<Item>(sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)])
var items: FetchedResults<Item>

var body: some View {

NavigationStack {
ZStack(alignment:.top){

if $showList.wrappedValue {
VStack{
List {
ForEach(items){item in
Text(item.name ?? "")
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
delete(item)
} label: {
Image(systemName:"trash")
}
.tint(.red)
}

}
}
}
.navigationBarTitle("Items")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing){
Button(action: {
showTextFieldAlert.toggle()

}){
Text("Add Item")
}
}
}
.foregroundColor(.cyan)
}
if $showTextFieldAlert.wrappedValue {
VStack{
CustomAlert(textFieldValue: $textFieldValue, showAlertWithTextField: $showTextFieldAlert,
title: "Insert item name", placeholder: "item name", handler: save)
}
}

}
}

}
func save(){
let entity = Item(context: viewContext)
entity.name = textFieldValue

do{
try PersistenceContainer.shared.save()
}catch{
fatalError(error.localizedDescription)
}
}
func delete(_ object: NSManagedObject){
withAnimation(.linear(duration: 0.2)) {
do{
try PersistenceContainer.shared.delete(object)
}catch{
fatalError(error.localizedDescription)
}
}
}
}

You can also to use @State var showList to hide the List when the alert is displayed: in this case you have to create the @Binding var showList in CustomAlert.swift .

I hope you enjoyed this tutorial and found it helpful.

--

--