Reputation: 1943
I'm trying to abstract an Alert which is being used in multiple places across my app.
I copied and pasted the implementation of func alert(isPresented: Binding<Bool>, content: () -> Alert) -> some View
and tweaked it to adapt it to my usage:
extension View {
func externalURLAlert(isPresented: Binding<Bool>, action: ()) -> some View {
isPresented.wrappedValue ? AnyView(Alert(
title: Text("alert.externalURL.title".localized),
message: Text("alert.externalURL.message".localized),
primaryButton: .cancel(),
secondaryButton: .default(Text("alert.externalURL.openAction.title".localized)) {
action
}
)) : AnyView(EmptyView())
}
}
My plan is to call it on a View like .externalURLAlert(isPresented: $isPresented, action: someAction)
but I'm failing to get the function to compile.
The error I'm getting is the following one:
Initializer 'init(_:)' requires that 'Alert' conform to 'View'
Upvotes: 5
Views: 15282
Reputation: 3561
You can customize to your own design.
Demo.swift
import SwiftUI
struct DemoView: View {
// MARK: - Value
// MARK: Private
@State private var isAlertPresented = false
// MARK: - View
// MARK: Public
var body: some View {
ZStack {
Button {
isAlertPresented = true
} label: {
Text("Alert test")
}
}
.alert(title: "title", message: "message",
primaryButton: CustomAlertButton(title: "Yes", action: { }),
secondaryButton: CustomAlertButton(title: "No", action: { }),
isPresented: $isAlertPresented)
}
}
#if DEBUG
struct DemoView_Previews: PreviewProvider {
static var previews: some View {
DemoView()
.previewDevice("iPhone 11 Pro")
}
}
#endif
CustomAlert.swift
import SwiftUI
struct CustomAlert: View {
// MARK: - Value
// MARK: Public
let title: String
let message: String
let dismissButton: CustomAlertButton?
let primaryButton: CustomAlertButton?
let secondaryButton: CustomAlertButton?
// MARK: Private
@State private var opacity: CGFloat = 0
@State private var backgroundOpacity: CGFloat = 0
@State private var scale: CGFloat = 0.001
@Environment(\.dismiss) private var dismiss
// MARK: - View
// MARK: Public
var body: some View {
ZStack {
dimView
alertView
.scaleEffect(scale)
.opacity(opacity)
}
.ignoresSafeArea()
.transition(.opacity)
.task {
animate(isShown: true)
}
}
// MARK: Private
private var alertView: some View {
VStack(spacing: 20) {
titleView
messageView
buttonsView
}
.padding(24)
.frame(width: 320)
.background(.white)
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.4), radius: 16, x: 0, y: 12)
}
@ViewBuilder
private var titleView: some View {
if !title.isEmpty {
Text(title)
.font(.system(size: 18, weight: .bold))
.foregroundColor(.black)
.lineSpacing(24 - UIFont.systemFont(ofSize: 18, weight: .bold).lineHeight)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
@ViewBuilder
private var messageView: some View {
if !message.isEmpty {
Text(message)
.font(.system(size: title.isEmpty ? 18 : 16))
.foregroundColor(title.isEmpty ? .black : .gray)
.lineSpacing(24 - UIFont.systemFont(ofSize: title.isEmpty ? 18 : 16).lineHeight)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private var buttonsView: some View {
HStack(spacing: 12) {
if dismissButton != nil {
dismissButtonView
} else if primaryButton != nil, secondaryButton != nil {
secondaryButtonView
primaryButtonView
}
}
.padding(.top, 23)
}
@ViewBuilder
private var primaryButtonView: some View {
if let button = primaryButton {
CustomAlertButton(title: button.title) {
animate(isShown: false) {
dismiss()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
button.action?()
}
}
}
}
@ViewBuilder
private var secondaryButtonView: some View {
if let button = secondaryButton {
CustomAlertButton(title: button.title) {
animate(isShown: false) {
dismiss()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
button.action?()
}
}
}
}
@ViewBuilder
private var dismissButtonView: some View {
if let button = dismissButton {
CustomAlertButton(title: button.title) {
animate(isShown: false) {
dismiss()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
button.action?()
}
}
}
}
private var dimView: some View {
Color.gray
.opacity(0.88)
.opacity(backgroundOpacity)
}
// MARK: - Function
// MARK: Private
private func animate(isShown: Bool, completion: (() -> Void)? = nil) {
switch isShown {
case true:
opacity = 1
withAnimation(.spring(response: 0.3, dampingFraction: 0.9, blendDuration: 0).delay(0.5)) {
backgroundOpacity = 1
scale = 1
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completion?()
}
case false:
withAnimation(.easeOut(duration: 0.2)) {
backgroundOpacity = 0
opacity = 0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
completion?()
}
}
}
}
#if DEBUG
struct CustomAlert_Previews: PreviewProvider {
static var previews: some View {
let dismissButton = CustomAlertButton(title: "OK")
let primaryButton = CustomAlertButton(title: "OK")
let secondaryButton = CustomAlertButton(title: "Cancel")
let title = "This is your life. Do what you want and do it often."
let message = """
If you don't like something, change it.
If you don't like your job, quit.
If you don't have enough time, stop watching TV.
"""
return VStack {
CustomAlert(title: title, message: message, dismissButton: nil, primaryButton: nil, secondaryButton: nil)
CustomAlert(title: title, message: message, dismissButton: dismissButton, primaryButton: nil, secondaryButton: nil)
CustomAlert(title: title, message: message, dismissButton: nil, primaryButton: primaryButton, secondaryButton: secondaryButton)
}
.previewDevice("iPhone 13 Pro Max")
.preferredColorScheme(.light)
}
}
#endif
CustomAlertButton.swift
import SwiftUI
struct CustomAlertButton: View {
// MARK: - Value
// MARK: Public
let title: LocalizedStringKey
var action: (() -> Void)? = nil
// MARK: - View
// MARK: Public
var body: some View {
Button {
action?()
} label: {
Text(title)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
.padding(.horizontal, 10)
}
.frame(height: 30)
.background(Color.purple)
.cornerRadius(15)
}
}
CustomAlertModifier.swift
import SwiftUI
struct CustomAlertModifier {
// MARK: - Value
// MARK: Private
@Binding private var isPresented: Bool
// MARK: Private
private let title: String
private let message: String
private let dismissButton: CustomAlertButton?
private let primaryButton: CustomAlertButton?
private let secondaryButton: CustomAlertButton?
}
extension CustomAlertModifier: ViewModifier {
func body(content: Content) -> some View {
content
.fullScreenCover(isPresented: $isPresented) {
CustomAlert(title: title, message: message, dismissButton: dismissButton, primaryButton: primaryButton, secondaryButton: secondaryButton)
}
}
}
extension CustomAlertModifier {
init(title: String = "", message: String = "", dismissButton: CustomAlertButton, isPresented: Binding<Bool>) {
self.title = title
self.message = message
self.dismissButton = dismissButton
self.primaryButton = nil
self.secondaryButton = nil
_isPresented = isPresented
}
init(title: String = "", message: String = "", primaryButton: CustomAlertButton, secondaryButton: CustomAlertButton, isPresented: Binding<Bool>) {
self.title = title
self.message = message
self.primaryButton = primaryButton
self.secondaryButton = secondaryButton
self.dismissButton = nil
_isPresented = isPresented
}
}
ViewExtension.swift
import SwiftUI
extension View {
func alert(title: String = "", message: String = "", dismissButton: CustomAlertButton = CustomAlertButton(title: "ok"), isPresented: Binding<Bool>) -> some View {
let title = NSLocalizedString(title, comment: "")
let message = NSLocalizedString(message, comment: "")
return modifier(CustomAlertModifier(title: title, message: message, dismissButton: dismissButton, isPresented: isPresented))
}
func alert(title: String = "", message: String = "", primaryButton: CustomAlertButton, secondaryButton: CustomAlertButton, isPresented: Binding<Bool>) -> some View {
let title = NSLocalizedString(title, comment: "")
let message = NSLocalizedString(message, comment: "")
return modifier(CustomAlertModifier(title: title, message: message, primaryButton: primaryButton, secondaryButton: secondaryButton, isPresented: isPresented))
}
}
Upvotes: 18
Reputation: 11416
try the following:
extension View {
@ViewBuilder
func externalURLAlert(isPresented: Binding<Bool>, action: ()) -> some View {
if isPresented.wrappedValue {
Alert(
title: Text("alert.externalURL.title".localized),
message: Text("alert.externalURL.message".localized),
primaryButton: .cancel(),
secondaryButton: .default(Text("alert.externalURL.openAction.title".localized)) { action }
)
}
}
Also try to use .sheet() -- this looks like you search for.
Usage sample: https://www.hackingwithswift.com/quick-start/swiftui/how-to-present-a-new-view-using-sheets
or any that you find by google -> "swiftUI usage .sheet"
Upvotes: 1
Reputation: 19874
The way modifiers work is by returning a modified version of the view they are called on. If you call Text("").foregroundColor(...)
, you receive a new Text
view with a new foreground color. It's the same with an alert, if you call Text("").alert(...
, you receive a Text
view that can display an alert on top.
Your modifier, on the other hand, completely erases that hierarchy and replaces it with either an empty view, or an alert, but this alert has no information on where it should be presented on.
If what you want is to display a standardized alert, you should leverage the existing modifier with your own parameter, like this:
extension View {
func externalURLAlert(isPresented: Binding<Bool>, action: ()) -> some View {
self.alert(isPresented: isPresented) {
Alert(
title: Text("alert.externalURL.title".localized),
message: Text("alert.externalURL.message".localized),
primaryButton: .cancel(),
secondaryButton: .default(Text("alert.externalURL.openAction.title".localized)) {
action()
}
)
}
}
}
Notice the use of self
, because we want to maintain the hierarchy, and .alert(...)
because we're using the existing system modifier that already knows how to display an alert.
Upvotes: 2