I would like to ask you how can I show alert to user.
I just tried:
.navigationBarItems(trailing: Button(action: {
let alert = Alert(title: Text("Add category"), message: Text("Do you want add category?"), primaryButton: Alert.Button.default(Text("Yes"), onTrigger: {
self.sceneries[0].sceneries.append(Scenery(name: "Name", imageName: "1"))
}), secondaryButton: Alert.Button.cancel())
self.presentation(self.$isShownAlert) { () -> Alert in
return alert
}
}, label: {
Text("Add category")
}))
But it shows me that it's unused and alert didn't appear...
You need to call presentation API on top of the view that should display the alert.
The best way to accomplish this is to have a #State variable, that tells SwiftUI whether the alert should be displayed or not.
The Button action would then set it to true, thus invalidating body, and triggering a view rebuilding.
struct ContentView : View {
#State var showAlert = false
var body: some View {
NavigationView {
List(0...10) { value in
Text(verbatim: "\(value)")
}
.navigationBarItems(leading: EmptyView(), trailing: Button(action: {
self.showAlert = true
}) {
Text(verbatim: "Show alert")
})
.navigationBarTitle(Text(verbatim: "A List"))
}
.presentation($showAlert) {
return Alert(title: Text(verbatim: "An Alert"))
}
}
}
In this example, the button sets the #State to true,
and presentation is called on the navigation view.
Result:
To display an alert with two buttons you can do like below:
#State var showAlert = false
let alert = Alert(title: Text("Title"), message: Text("Alert message"),
primaryButton: Alert.Button.default(Text("OK"),
onTrigger: {
print("OK button tapped")
}
),
secondaryButton: Alert.Button.cancel()
)
var body: some View {
NavigationView {
Text("Content")
.navigationBarItems(trailing: Button(action: {
self.showAlert = true
}, label: {
Text("Show Alert")
}).presentation(self.$showAlert, alert: {
return alert
})
)
}
}
Result:
Answers above are already deprecated. Use this instead:
#State var showsAlert = false
var body: some View {
NavigationView {
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.navigationBarTitle("My List", displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.showsAlert = true
}, label: {
Text("Show Alert")
}).alert(isPresented: self.$showsAlert) {
Alert(title: Text("Hello World"))
}
)
}
}
Note the use of .alert instead of .presentation.
Related
I want to use an alert with textField in my swiftUI app so I made following wrapper for UIAlertController. I want to disable my save button when there is no text in textField and enable it the moment user enters some text, but for some reason it is not happening, the button is stuck to the initial state.
code
struct UIAlertControllerWrapper: UIViewControllerRepresentable {
#Binding var text: String
#Binding var showAlert: Bool
var title: String
var message: String
var placeholder: String
var action: () -> Void
func makeUIViewController(context: Context) -> some UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
if showAlert {
let alert = createAlert(context)
context.coordinator.alert = alert
alert.actions[1].isEnabled = false
DispatchQueue.main.async {
uiViewController.present(alert, animated: true) {
showAlert = false
}
}
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: UIAlertControllerWrapper
var alert: UIAlertController?
init(_ parent: UIAlertControllerWrapper) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text as? NSString {
parent.text = text.replacingCharacters(in: range, with: string)
} else {
parent.text = ""
}
guard let alert = alert else {
return true
}
if parent.text.trimmingCharacters(in: .whitespaces).isEmpty {
alert.actions[1].isEnabled = false
} else {
alert.actions[1].isEnabled = true
}
return true
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func createAlert(_ context: Context) -> UIAlertController {
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: .alert
)
alert.addTextField { textField in
textField.placeholder = placeholder
textField.text = text
textField.delegate = context.coordinator
}
let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in
alert.dismiss(animated: true) {
showAlert = false
}
text = ""
}
let save = UIAlertAction(title: "Save", style: .default) { _ in
action()
alert.dismiss(animated: true) {
showAlert = false
}
text = ""
}
alert.addAction(cancel)
alert.addAction(save)
return alert
}
}
I need to use UIAlertContoller, as SwiftUI's Alert does not support TextField.
I must not use Custom created AlertView, due to various reason(Accessibility, DynamicType, Dark Mode support etc).
Basic Idea is, SwiftUI's alert must hold TextField & entered text must be reflect back for usage.
I created a SwiftUI view by Conforming to UIViewControllerRepresentable following is working code.
struct AlertControl: UIViewControllerRepresentable {
typealias UIViewControllerType = UIAlertController
#Binding var textString: String
#Binding var show: Bool
var title: String
var message: String
func makeUIViewController(context: UIViewControllerRepresentableContext<AlertControl>) -> UIAlertController {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = "Enter some text"
}
let cancelAction = UIAlertAction(title: "cancel", style: .destructive) { (action) in
self.show = false
}
let submitAction = UIAlertAction(title: "Submit", style: .default) { (action) in
self.show = false
}
alert.addAction(cancelAction)
alert.addAction(submitAction)
return alert
}
func updateUIViewController(_ uiViewController: UIAlertController, context: UIViewControllerRepresentableContext<AlertControl>) {
}
func makeCoordinator() -> AlertControl.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var control: AlertControl
init(_ control: AlertControl) {
self.control = control
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text {
self.control.textString = text
}
return true
}
}
}
// SwiftUI View in some content view
AlertControl(textString: self.$text,
show: self.$showAlert,
title: "Title goes here",
message: "Message goes here")
Problem:
There is No activity in Alert Action when it is tapped. I put breakpoints to check, but it never hit there.
Even UITextFieldDelegate's function never hit.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
Edit: cancelAction or submitAction does not triggers on tap of these fields.
Here is full demo module for solution that works. Tested with Xcode 11.4 / iOS 13.4
See also important comments inline
struct AlertControl: UIViewControllerRepresentable {
#Binding var textString: String
#Binding var show: Bool
var title: String
var message: String
func makeUIViewController(context: UIViewControllerRepresentableContext<AlertControl>) -> UIViewController {
return UIViewController() // holder controller - required to present alert
}
func updateUIViewController(_ viewController: UIViewController, context: UIViewControllerRepresentableContext<AlertControl>) {
guard context.coordinator.alert == nil else { return }
if self.show {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
context.coordinator.alert = alert
alert.addTextField { textField in
textField.placeholder = "Enter some text"
textField.text = self.textString // << initial value if any
textField.delegate = context.coordinator // << use coordinator as delegate
}
alert.addAction(UIAlertAction(title: "cancel", style: .destructive) { _ in
// your action here
})
alert.addAction(UIAlertAction(title: "Submit", style: .default) { _ in
// your action here
})
DispatchQueue.main.async { // must be async !!
viewController.present(alert, animated: true, completion: {
self.show = false // hide holder after alert dismiss
context.coordinator.alert = nil
})
}
}
}
func makeCoordinator() -> AlertControl.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var alert: UIAlertController?
var control: AlertControl
init(_ control: AlertControl) {
self.control = control
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text as NSString? {
self.control.textString = text.replacingCharacters(in: range, with: string)
} else {
self.control.textString = ""
}
return true
}
}
}
// Demo view for Alert Controll
struct DemoAlertControl: View {
#State private var text = ""
#State private var showAlert = false
var body: some View {
VStack {
Button("Alert") { self.showAlert = true }
.background(AlertControl(textString: self.$text, show: self.$showAlert,
title: "Title goes here", message: "Message goes here"))
Text(self.text)
}
}
}
I'm trying to get this alert to work for the last two hours. I don't know why it would't. Is it even possible for me to make it work right there?
class TimerHolder : ObservableObject {
#State var showResults = false
var timer : Timer!
#Published var count = 0
func start() {
self.timer?.invalidate()
self.count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
_ in
// self.count += 1
// print(self.count)
if self.count < 60 {
self.count += 1
print(self.count)
}else{
_ = Alert(title: Text("Warning"), message: Text("The timer will start as soon as you press OK. "), primaryButton: .default(Text("OK"), action: {})
, secondaryButton: .cancel(Text("Not yet.")))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Seems you are making three lefts to turn right. The ObservableObject should only be a holder for data.
I seems that it would be better that you do this outside of an ObservableObject and that the Timer is held in a View or a ViewController.
class StartViewController: UIViewController {
var displayTimer:Timer!
override func viewDidLoad() {
super.viewDidLoad()
//setup the Timer
displayTimer = Timer.scheduledTimer(timeInterval: 60.0,
target: self,
selector: #selector(self.displayTimerFired(_:)),
userInfo: nil,
repeats: false)
}
#objc func displayTimerFired(_ timer: Timer) {
let alertController = UIAlertController(title: "60 Seconds...", message: "60 Seconds have elapsed.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
//clean up timer
displayTimer.invalidate()
}
}
EDIT
Sorry just realized you are using SwiftUI
struct TimerView: View {
let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
var body: some View {
.onReceive(timer) { _ in
_ = Alert(title: Text("Warning"), message: Text("The timer will start as soon as you press OK. "), primaryButton: .default(Text("OK"), action: {})
, secondaryButton: .cancel(Text("Not yet.")))
}
}
}
Alert in SwiftUI should be presented via .alert modifier in view hierarchy, so it should be like the following
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = 0
func start() {
self.timer?.invalidate()
self.count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
_ in
if (self.count < 60) {
self.count += 1 // << no views here, only calculations
}
}
}
func stop() {
self.timer?.invalidate()
}
}
somewhere in view
struct DemoView : View {
#ObservedObject var durationTimer = TimerHolder()
#State private var showAlert = false // state of presenting alert
var body: some View {
VStack{
Text("\(durationTimer.count) Seconds")
}
.onReceive(durationTimer.$count) { count in // << listen for timer
if count == 60 { // << show alert
durationTimer.stop()
self.showAlert = true
}
}
.alert(isPresented: $showAlert) { // alert section, condition + construction
Alert(title: Text("Warning"), message: Text("The timer will start as soon as you press OK. "), primaryButton: .default(Text("OK"), action: {})
, secondaryButton: .cancel(Text("Not yet.")))
}
}
}
In the code below, I am trying to figure out how to execute the savePhoto function with a SwiftUI button. This is actually the simplified version of the problem I am trying to solve, but still can't figure this out. The button I created in the ContentView is how I would imagine to solve the problem, but because of the SwiftUI structs, I can't seem to find a way around it.
Here is my SwiftUI view:
struct ContentView: View {
var body: some View {
ZStack{
AnotherControllerView()
Button(action: { AnotherControllerView.savePhoto() }){
Text("Save Photo")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And my integrated UIViewController:
import Foundation
import SwiftUI
import UIKit
struct AnotherControllerView : UIViewControllerRepresentable {
typealias UIViewControllerType = AnotherController
func makeCoordinator() -> AnotherControllerView.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<AnotherControllerView>) -> AnotherController {
return AnotherController()
}
func updateUIViewController(_ uiViewController: AnotherController, context: UIViewControllerRepresentableContext<AnotherControllerView>) {
}
class Coordinator : NSObject {
var parent : AnotherControllerView
init(_ viewController : AnotherControllerView){
self.parent = viewController
}
}
}
class AnotherController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.blue
}
func savePhoto(){
let alert = UIAlertController(title: "Save Photo to Camera Roll", message: "Would you like to save your drawing to the camera roll?", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Save", style: .default, handler: someHandler))
self.present(alert, animated: true)
}
func someHandler(alert: UIAlertAction!) {
print("Handler executed")
}
}
Hello to control the UIViewController we need to create a bind between the View and UIViewControllerRepresentable
let explain it in code:
first you need to declare a new variable annotated with #Binding inside the AnotherControllerView
it will be like this :
struct AnotherControllerView : UIViewControllerRepresentable {
#Binding var isShown: Bool
typealias UIViewControllerType = AnotherController
func makeCoordinator() -> AnotherControllerView.Coordinator {
Coordinator()
}
func makeUIViewController(context: UIViewControllerRepresentableContext<AnotherControllerView>) -> AnotherController {
return AnotherController(isShown: $isShown)
}
func updateUIViewController(_ controller: AnotherController, context: UIViewControllerRepresentableContext<AnotherControllerView>) {
if(self.isShown){
controller.savePhoto()
}
}
class Coordinator : NSObject {
}
}
so for that in the updateUIViewController we implement the logic there
class AnotherController : UIViewController {
#Binding var isShown: Bool
init(isShown: Binding<Bool>) {
_isShown = isShown
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.blue
}
func savePhoto(){
let alert = UIAlertController(title: "Save Photo to Camera Roll", message: "Would you like to save your drawing to the camera roll?", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: cancelAlert))
alert.addAction(UIAlertAction(title: "Save", style: .default, handler: someHandler))
self.present(alert, animated: true)
}
func cancelAlert(alert: UIAlertAction!) {
self.isShown = false
}
func someHandler(alert: UIAlertAction!) {
print("Handler executed")
}
}
and if you're trying to capture an image let me show you how I implemented this:
struct CaptureImageView {
#Binding var isShown: Bool
#Binding var image: Image?
var sourceType: UIImagePickerController.SourceType
func makeCoordinator() -> Coordinator {
return Coordinator(isShown: $isShown, image: $image)
}
}
extension CaptureImageView: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<CaptureImageView>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = sourceType
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<CaptureImageView>) {
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding var isCoordindatorShown: Bool
#Binding var imageInCoordinator: Image?
init(isShown: Binding<Bool>, image: Binding<Image?>) {
_isCoordindatorShown = isShown
_imageInCoordinator = image
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let unwrapImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else {return}
imageInCoordinator = Image(uiImage: unwrapImage)
isCoordindatorShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isCoordindatorShown = false
}
}
}
and in the body I just call :
CaptureImageView(isShown: $showCaptureImageView, image: $imageCaptured, sourceType: importSourceType)
If the AnotherControllerView is large and you use it scarcely in the new, you may consider use a simpler workaround.
struct ContentAnotherControllerView: View {
let anotherControllerView = AnotherControllerView()
var body: some View {
ZStack{
anotherControllerView
Button(action: { self.anotherControllerView.savePhoto() }){
Text("Save Photo")
}
}
}
}
struct AnotherControllerView : UIViewControllerRepresentable {
typealias UIViewControllerType = AnotherController
var savePhotoDelegate: AnotherController? = AnotherController()
func savePhoto(){
savePhotoDelegate?.savePhoto()
}
func makeUIViewController(context: UIViewControllerRepresentableContext<AnotherControllerView>) -> AnotherController {
if let savePhotoDelegate = savePhotoDelegate{
return savePhotoDelegate
}
return AnotherController()
}
....}
This approach is just to remind us one view can be reused in some situation. As the UIView bridge is not a pure SwiftUI domain, you can try this way.
How can I create a popup menu like the one present in WhatsApp?
Sorry for the dumb question, but I don't even know what to search. I'm pretty sure it's not a UIPickerView.
This is an action sheet. Here's the documentation about it in the iOS Human Interface Guidelines.
You can make one like this:
SwiftUI (iOS 15 and above)
Use confirmationDialog(). Here is the official documentation for it and here are some real-world examples, which are partially the source of the example code.
#State private var shouldShowActionSheet = false
<custom view>
.confirmationDialog("", isPresented: $shouldShowActionSheet) {
Button("Option 1") {
<handler>
}
Button("Option 2") {
<handler>
}
Button("Cancel", role: .cancel) { }
}
SwiftUI (iOS 13 and 14)
#State private var shouldShowActionSheet = false
[...]
<custom view>
.actionSheet(isPresented: $shouldShowActionSheet) {
ActionSheet(
title: Text(""),
buttons: [
.default(Text("Option 1")) {
<handler>
},
.default(Text("Option 2")) {
<handler>
},
.cancel()
]
)
}
UIKit
let alert = UIAlertController(
title: nil,
message: nil,
preferredStyle: .actionSheet
)
alert.addAction(
.init(title: "Action 1", style: .default) { _ in
<handler>
}
)
alert.addAction(
.init(title: "Action 1", style: .default) { _ in
<handler>
}
)
present(alert, animated: true)
Its UIAlertController with preferredStyle - UIAlertControllerStyle.actionSheet
https://developer.apple.com/documentation/uikit/uialertcontroller