Anyone an idea how to create an Alert in SwiftUI that contains a TextField?
As already was mentioned Alert
is provide not many functionality and so almost useless in any non-standard cases when using in SwiftUI.
I ended up with a bit extensive solution - View
that may behave as alert with high customisation level.
Create ViewModel
for popUp:
struct UniAlertViewModel {
let backgroundColor: Color = Color.gray.opacity(0.4)
let contentBackgroundColor: Color = Color.white.opacity(0.8)
let contentPadding: CGFloat = 16
let contentCornerRadius: CGFloat = 12
}
we also need to configure buttons, for this purpose let's add one more type:
struct UniAlertButton {
enum Variant {
case destructive
case regular
}
let content: AnyView
let action: () -> Void
let type: Variant
var isDestructive: Bool {
type == .destructive
}
static func destructive(
@ViewBuilder content: @escaping () -> Content
) -> UniAlertButton {
UniAlertButton(
content: content,
action: { /* close */ },
type: .destructive)
}
static func regular(
@ViewBuilder content: @escaping () -> Content,
action: @escaping () -> Void
) -> UniAlertButton {
UniAlertButton(
content: content,
action: action,
type: .regular)
}
private init(
@ViewBuilder content: @escaping () -> Content,
action: @escaping () -> Void,
type: Variant
) {
self.content = AnyView(content())
self.type = type
self.action = action
}
}
add View that can become our customizable popUp:
struct UniAlert: View where Presenter: View, Content: View {
@Binding private (set) var isShowing: Bool
let displayContent: Content
let buttons: [UniAlertButton]
let presentationView: Presenter
let viewModel: UniAlertViewModel
private var requireHorizontalPositioning: Bool {
let maxButtonPositionedHorizontally = 2
return buttons.count > maxButtonPositionedHorizontally
}
var body: some View {
GeometryReader { geometry in
ZStack {
backgroundColor()
VStack {
Spacer()
ZStack {
presentationView.disabled(isShowing)
let expectedWidth = geometry.size.width * 0.7
VStack {
displayContent
buttonsPad(expectedWidth)
}
.padding(viewModel.contentPadding)
.background(viewModel.contentBackgroundColor)
.cornerRadius(viewModel.contentCornerRadius)
.shadow(radius: 1)
.opacity(self.isShowing ? 1 : 0)
.frame(
minWidth: expectedWidth,
maxWidth: expectedWidth
)
}
Spacer()
}
}
}
}
private func backgroundColor() -> some View {
viewModel.backgroundColor
.edgesIgnoringSafeArea(.all)
.opacity(self.isShowing ? 1 : 0)
}
private func buttonsPad(_ expectedWidth: CGFloat) -> some View {
VStack {
if requireHorizontalPositioning {
verticalButtonPad()
} else {
Divider().padding([.leading, .trailing], -viewModel.contentPadding)
horizontalButtonsPadFor(expectedWidth)
}
}
}
private func verticalButtonPad() -> some View {
VStack {
ForEach(0.. some View {
HStack {
let sidesOffset = viewModel.contentPadding * 2
let maxHorizontalWidth = requireHorizontalPositioning ?
expectedWidth - sidesOffset :
expectedWidth / 2 - sidesOffset
Spacer()
if !requireHorizontalPositioning {
ForEach(0..
to simplify usage let's add extension to View
:
extension View {
func assemblyAlert(
isShowing: Binding,
viewModel: UniAlertViewModel,
@ViewBuilder content: @escaping () -> Content,
actions: [UniAlertButton]
) -> some View where Content: View {
UniAlert(
isShowing: isShowing,
displayContent: content(),
buttons: actions,
presentationView: self,
viewModel: viewModel)
}
}
And usage:
struct ContentView: View {
@State private var isShowingAlert: Bool = false
@State private var text: String = ""
var body: some View {
VStack {
Button(action: {
withAnimation {
isShowingAlert.toggle()
}
}, label: {
Text("Show alert")
})
}
.assemblyAlert(isShowing: $isShowingAlert,
viewModel: UniAlertViewModel(),
content: {
Text("title")
Image(systemName: "phone")
.scaleEffect(3)
.frame(width: 100, height: 100)
TextField("enter text here", text: $text)
Text("description")
}, actions: buttons)
}
}
}
Demo: