I going learn swift and swiftUI.
I make application for organize notes by category. you can find my project in my GitHub if you need. https://github.com/yoan8306/List-Notes
I have problem. I think it's simple. I would like make 2 alerts messages. The first it's when save is success and the second is when they are problem like one field is empty or category is empty.
private func checkNoteIsOk() -> Bool{
if !noteTitleField.isEmpty && !noteField.isEmpty && categorySelected != nil {
return true
} else {
return false
}
}
.
Button(action: {
guard checkNoteIsOk() else {
presentAlert = true
return
}
coreDM.saveNote(noteData: noteField, noteTitle: noteTitleField,
noteDate: Date(), noteCategory: categorySelected!)
emptyField()
saveSuccess = true
},
label: {
Text("Save")
}
)
}
//end Vstak
.navigationTitle("Create new note")
.alert(isPresented: $presentAlert) {
Alert(title: Text("Error !"), message: Text("Not saved"),
dismissButton: .default(Text("OK"))) }
.alert(isPresented: $saveSuccess) {
Alert(title: Text("Success !"), message: Text("Insert with success !"),
dismissButton: .default(Text("OK"))) }
I think it's because they are two alerts messages. And only the last message alert can display. Thank you for your answer and your help.
For multiple alerts in a single view, you can use an enum.
First, you need to create an enum like this and define all the alert message
enum AlertType: Identifiable {
var id: UUID {
return UUID()
}
case success
case error
var title: String {
switch self {
case .success:
return "Success !"
case .error:
return "Error !"
}
}
var message: String {
switch self {
case .success:
return "Insert with success !"
case .error:
return "This category already exist !!"
}
}
}
now create one state var in the view.
struct NewCategoryView: View {
#State private var alertType: AlertType?
// Other code
}
and add the alert at the end
//end Vstak
.navigationTitle("New Category")
.onAppear(perform: { updateCategoryList()} )
.alert(item: self.$alertType, content: { (type) -> Alert in
Alert(title: Text(type.title), message: Text(type.message),
dismissButton: .default(Text("OK")))
})
now show the alert by assigning the value. Like this
if condition_true {
alertType = AlertType.success //<-- Here
} else {
alertType = AlertType.error //<-- Here
}
While [Raja]'s answer is working. I don't think it is ideal because
it generates random UUID's which Apple discourages when it's not needed.
it does requires multiple switch statements where only one is needed.
A more simple solution might be to define the enum like this
enum ResultAlert: Int8, Identifiable {
case success, error
var id: some Hashable { rawValue }
var content: Alert {
switch self {
case .success: return Alert(title: Text("Success!"))
case .error: return Alert(title: Text("Oy, error..."))
}
}
}
Then the rest is the same as Raja's answer:
Add it as a #State variable to your view
#State var resultAlert: ResultAlert?
Activate it using resultAlert = .success or resultAlert = .error. Deactivate it using resultAlert = .none
And present it like this:
.alert(item: $resultAlert, content: \.content)
Related
I am creating an application in which a user, based on the permissions he has, can access the various views.
I use this method to constantly check user permissions:
func checkPermission() {
let docRef = self.DatabaseFirestore.collection("Admins").document(phoneNumber)
docRef.getDocument{(document, error) in
guard error == nil else {
return
}
if let document = document, document.exists {
self.controlloAdmin = true
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.permission = data["Permessi"] as? [Bool] ?? []
} else {
self.controlloAdmin = false
self.isRegistred = false
self.access = false
}
}
}
I don't know if it is the most correct function I could use, but it is one of the few that I have found that works.
This is my view:
struct AdministratorPage: View {
#StateObject var administratorManager = AdministratorManager()
// User variables.
#AppStorage("phoneNumber") var phoneNumber: String = "" // User number.
#AppStorage("Access") var access: Bool = false
var body: some View {
administratorManager.checkPermission()
return NavigationView {
HStack {
VStack {
Text("Home")
Text(phoneNumber)
// Button to log out.
Button("Logout", action: {
self.access = false
})
Button("Alert", action: {
administratorManager.message = "Error title!"
administratorManager.message = "Error message!"
administratorManager.isMessage = true
}).alert(isPresented: $administratorManager.isMessage) {
Alert(title: Text(administratorManager.title), message: Text(administratorManager.message),
dismissButton: .default(Text("Ho capito!")))
}
}
}
}
}
}
When I call the "administratorManager.checkPermission()" function and press the "Alert" button the message is displayed, but even if the button is pressed the alert does not disappear. If I don't call this function, everything works.
How can I solve? Can the alert go against firebase? Is there a more suitable method to read only one data?
photo of the screen when it got locked
I ran your code and I saw the behavior you described.
The reason is the function call directly in the body.
If you want to call a function when when you open a view, use the .onAppear function for that specific view. In your case
.onAppear {
administratorManager.checkPermission()
}
The following (worked for me with you code):
struct AdministratorPage: View {
#StateObject var administratorManager = AdministratorManager()
// User variables.
#AppStorage("phoneNumber") var phoneNumber: String = "" // User number.
#AppStorage("Access") var access: Bool = false
var body: some View {
return NavigationView {
HStack {
VStack {
Text("Home")
Text(phoneNumber)
// Button to log out.
Button("Logout", action: {
self.access = false
})
Button("Alert", action: {
administratorManager.message = "Error title!"
administratorManager.message = "Error message!"
administratorManager.isMessage = true
}).alert(isPresented: $administratorManager.isMessage) {
Alert(title: Text(administratorManager.title), message: Text(administratorManager.message),
dismissButton: .default(Text("Ho capito!")))
}
}
}
}
.onAppear {
administratorManager.checkPermission()
}
}
}
UPDATE: add Snapshot listener instead of polling
Your initial approach was doing a kind of polling, it called the function constantly. Please keep in mind, when you do a Firebase request, you will be billed for the documents you get back. If you do the polling, you get the same document multiple times and will be billed for it.
With my above mentioned example in this answer, you just call the function once.
If you now want to get the live updated from Firestore, you can add a snapshot listener. The approach would be:
func checkPermission() {
let docRef = db.collection("Admins").document(phoneNumber).addSnapshotListener() { documentSnapshot, error in //erca nella collezione se c'è il numero.
guard error == nil else {
print("ERROR.")
return
}
if let document = documentSnapshot {
self.controlloAdmin = true
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.permission = data["Permessi"] as? [Bool] ?? []
} else {
self.controlloAdmin = false
self.isRegistred = false
self.access = false
}
}
}
Whenever a value changed on that document in Friestore, it'll be changed on your device as well.
Best, Sebastian
I want to delete list items and when I delete list items, it will show confirmation dialog like .alert dialog. I have code below and if I want to remove list item .alert dialog is work, but if I try to remove all list items, .alert dialog not work, and I am not able to remove all items, I do not know where I missed? I guess most probably it is due to the I have two .alert dialog and they are conflicted, any idea?
struct CustomView: View {
#State private var selectedUsers: CustomModel?
#State var users: [CustomModel]
#State private var selectDelete = false
#State private var selectAllDelete = false
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(content: {
ForEach(users){ user in
CustomRowView(user: user)
.contextMenu {
Button(action: {
selectDelete = true
}) {
Text("remove")
}
Button(action: {
selectAllDelete = true
}) {
Text("remove all")
}
}
.alert(isPresented: $selectDelete) {
Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.delete(item: data)
},
secondaryButton: .cancel()
)
}
.alert(isPresented: $selectAllDelete) {
Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.datas.removeAll()
},
secondaryButton: .cancel()
)
}
.onDelete { (indexSet) in
self.users.remove(atOffsets: indexSet)
}
}
})
})
}
private func delete(item user: CustomModel) {
if let index = users.firstIndex(where: { $0.id == user.id }) {
users.remove(at: index)
}
}
}
model:
struct CustomModel: Identifiable{
var id = UUID().uuidString
var name: String
}
var users = [
CustomModel(name: "david"),
CustomModel(name: "marry"),
CustomModel(name: "henry"),
CustomModel(name: "nadi"), ]
You can create an alert type and handle it using switch statement.
enum AlertType {
case selectDelete
case selectAllDelete
}
private var alertType: AlertType?
#State private var isAlertPresented = false
...
Button(action: {
alertType = .selectDelete
isAlertPresented = true
}) {
Text("remove all")
}
...
.alert(isPresented: $isAlertPresented) {
presentAlert()
}
...
func presentAlert() -> Alert {
switch alertType {
case .selectDelete:
return Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.delete(item: data)
},
secondaryButton: .cancel())
case .selectAllDelete:
return Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.datas.removeAll()
},
secondaryButton: .cancel())
default:
return Alert(title: Text(""))
}
}
If you apply the modifier to each Button it'll work. Also, you might find confirmationDialog more suitable for this task.
Move your Buttons into custom Views will help too because body has a 10 View limit.
I've done a ton of searching and read a bunch of articles but I cannot get SwiftUI to dynamically update the view based on changing variables in the model, at least the kind of thing I'm doing. Basically I want to update the view based on the app's UNNotificationSettings.UNAuthorizationStatus. I have the app check the status on launch and display the status. If the status is not determined, then tapping on the text will trigger the request notifications dialog. However, the view doesn't update after the user either permits or denies the notifications. I'm sure I'm missing something fundamental because I've tried it a dozen ways, including with #Published ObservableObject, #ObservedObject, #EnvironmentObject, etc.
struct ContentView: View {
#EnvironmentObject var theViewModel : TestViewModel
var body: some View {
VStack {
Text(verbatim: "Notifications are: \(theViewModel.notificationSettings.authorizationStatus)")
.padding()
}
.onTapGesture {
if theViewModel.notificationSettings.authorizationStatus == .notDetermined {
theViewModel.requestNotificationPermissions()
}
}
}
}
class TestViewModel : ObservableObject {
#Published var notificationSettings : UNNotificationSettings
init() {
notificationSettings = type(of:self).getNotificationSettings()!
}
func requestNotificationPermissions() {
let permissionsToRequest : UNAuthorizationOptions = [.alert, .sound, .carPlay, .announcement, .badge]
UNUserNotificationCenter.current().requestAuthorization(options: permissionsToRequest) { granted, error in
if granted {
print("notification request GRANTED")
}
else {
print("notification request DENIED")
}
if let error = error {
print("Error requesting notifications:\n\(error)")
}
else {
DispatchQueue.main.sync {
self.notificationSettings = type(of:self).getNotificationSettings()!
}
}
}
}
static func getNotificationSettings() -> UNNotificationSettings? {
var settings : UNNotificationSettings?
let start = Date()
let semaphore = DispatchSemaphore(value: 0)
UNUserNotificationCenter.current().getNotificationSettings { notificationSettings in
settings = notificationSettings
semaphore.signal()
}
semaphore.wait()
while settings == nil {
let elapsed = start.distance(to: Date())
Thread.sleep(forTimeInterval: TimeInterval(0.001))
if elapsed > TimeInterval(1) {
print("ERROR: did not get notification settings in less than a second, giving up!")
break
}
}
if settings != nil {
print("\(Date()) Notifications are: \(settings!.authorizationStatus)")
}
return settings
}
}
func getUNAuthorizationStatusString(_ authStatus : UNAuthorizationStatus) -> String {
switch authStatus {
case .notDetermined: return "not determined"
case .denied: return "denied"
case .authorized: return "authorized"
case .provisional: return "provisional"
case .ephemeral: return "ephemeral"
#unknown default: return "unknown case with rawValue \(authStatus.rawValue)"
}
}
extension UNAuthorizationStatus : CustomStringConvertible {
public var description: String {
return getUNAuthorizationStatusString(self)
}
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ authStatus: UNAuthorizationStatus) {
appendLiteral(getUNAuthorizationStatusString(authStatus))
}
}
EDIT: I tried adding objectWillChange but the view still isn't updating.
class TestViewModel : ObservableObject {
let objectWillChange = ObservableObjectPublisher()
#Published var notificationSettings : UNNotificationSettings {
willSet {
objectWillChange.send()
}
}
init() {
notificationSettings = type(of:self).getNotificationSettings()!
}
Per the apple docs the properties wrappers like #Published should hold values. UNNotificationSettings is a reference type. Since the class gets mutated and the pointer never changes, #Publushed has no idea that you changed anything. Either publish a value (it make a struct and init it from he class) or manually send the objectwillChange message manually.
While I was not able to get it to work with manually using objectWillChange, I did create a basic working system as follows. Some functions are not repeated from the question above.
struct TestModel {
var notificationAuthorizationStatus : UNAuthorizationStatus
init() {
notificationAuthorizationStatus = getNotificationSettings()!.authorizationStatus
}
}
class TestViewModel : ObservableObject {
#Published var theModel = TestModel()
func requestAndUpdateNotificationStatus() {
requestNotificationPermissions()
theModel.notificationAuthorizationStatus = getNotificationSettings()!.authorizationStatus
}
}
struct ContentView: View {
#ObservedObject var theViewModel : TestViewModel
var body: some View {
VStack {
Button("Tap to update") {
theViewModel.requestAndUpdateNotificationStatus()
}
.padding()
switch theViewModel.theModel.notificationAuthorizationStatus {
case .notDetermined: Text("Notifications have not been requested yet.")
case .denied: Text("Notifications are denied.")
case .authorized: Text("Notifications are authorized.")
case .provisional: Text("Notifications are provisional.")
case .ephemeral: Text("Notifications are ephemeral.")
#unknown default: Text("Notifications status is an unexpected state.")
}
}
}
}
I have to present an Alert on a view if the user taps on it.
My alert depends on several situations:
Item is purchased. Show the item.
Item have not be purchased. Show an alert telling the user the item have to be purchased. This alert must show two buttons, OK to purchase, Cancel to dismiss.
User taps to purchase the item.
Purchase is successful, show the item.
Purchase fails, show error.
This is how I did it.
class AlertDialog {
enum SelectedType {
case none
case purchase
case mustBePurchased
case purchaseError
}
var selectedType:SelectedType = .none
}
struct FilteredListItem: View {
#State var showAlert: Bool = false
private var alertDialog:AlertDialog?
var body: some View {
Text(item.termLowerCase)
.font(fontItems)
.foregroundColor(.white)
.onTapGesture {
DispatchQueue.main.async {
appStoreWrapper.verifyPurchase(productID: item.package!)
{ // run if purchased
purchased = true
} runIfNotPurchased: {
purchased = false
alertDialog!.selectedType = .mustBePurchased
showAlert = true
}
}
}
.alert(isPresented: $showAlert) {
if alertDialog!.selectedType == .purchase {
appStoreWrapper.purchase(productID: item.package!) {
// run if purchased
purchased = true
} runIfPurchaseFailed: { (error) in
alertDialog!.selectedType = .purchaseError
appStoreWrapper.purchaseError = error
showAlert = true
}
} else if alertDialog!.selectedType == .purchaseError {
let primaryButton = Alert.Button.default(Text("OK")) {
showAlert = false
}
return Alert(title: Text(appStoreWrapper.makeString("ERROR")),
message: Text(appStoreWrapper.purchaseError),
dismissButton: primaryButton)
}
let dismissButton = Alert.Button.default(Text(appStoreWrapper.makeString("CANCEL"))) {
showAlert = false
}
let primaryButton = Alert.Button.default(Text("OK")) {
appStoreWrapper.purchase(productID: item.package!) {
// run if purchased
purchased = true
} runIfPurchaseFailed: { (error) in
appStoreWrapper.purchaseError = error
alertDialog!.selectedType = .purchaseError
showAlert = true
print(erro)
}
}
return Alert(title: Text(appStoreWrapper.makeString("ERROR")),
message: Text(appStoreWrapper.purchaseError),
primaryButton: primaryButton,
secondaryButton: dismissButton)
}
This is my problem: the modifier .alert(isPresented: $showAlert) expects an Alert() to be returned, right? But I have these asynchronous methods
appStoreWrapper.verifyPurchase(productID: item.package!)
{ // run if purchased },
runIfNotPurchased: { }
that cannot return anything to the alert modifier. How do I do that? Is what I am doing right?
There's a lot going on in your code and you didn't post the code for appStoreWrapper, but here's some code that should be able to point you in the right direction.
FYI:
You can use a Button with an Action instead of using Text with .onTapGesture
The code within .Alert should only function to get an Alert. You shouldn't be doing other actions within the .Alert closure.
struct FilteredListItem: View {
#State var showAlert: Bool = false
private var alertDialog: AlertDialog?
var body: some View {
Button(action: {
verifyItem()
}, label: {
Text("ITEM NAME")
.foregroundColor(.white)
})
.accentColor(.primary)
.alert(isPresented: $showAlert, content: {
getAlert()
})
}
func verifyItem() {
// FUNCTION TO VERIFY ITEM HERE
var success = true //appStoreWrapper.verifyPurchase...
if success {
// Handle success
} else {
alertDialog?.selectedType = .mustBePurchased
showAlert.toggle()
}
}
func purchaseItem() {
// FUNCTION TO PURCHASE ITEM HERE
var success = true //appStoreWrapper.purchase...
if success {
// Handle success
} else {
alertDialog?.selectedType = .purchaseError
showAlert.toggle()
}
}
func getAlert() -> Alert {
guard let dialog = alertDialog else {
return Alert(title: Text("Error getting alert dialog."))
}
switch dialog.selectedType {
case .purchaseError:
return Alert(
title: Text("Error purchasing item."),
message: nil,
dismissButton: .default(Text("OK")))
case .mustBePurchased:
return Alert(
title: Text("Items have to be purchased."),
message: nil,
primaryButton: .default(Text("Purchase"), action: {
purchaseItem()
}),
secondaryButton: .cancel())
case .none, .purchase:
return Alert(title: Text("Purchased!"))
}
}
}
I've been able to get a rudimentary version of Face / Touch ID working inside my app. However, I want to add better fallbacks and error handling.
So I've been researching how to do it. There are fantastic resources like this:
Face ID evaluation process not working properly
However, I can't find anything that works inside a SwiftUI view. At the moment my project won't run with:
'unowned' may only be applied to class and class-bound protocol types, not 'AuthenticateView'
and
Value of type 'AuthenticateView' has no member 'present'
any help would be much appreciated. Thank you!
Here's my code inside AuthenticateView.swift
func Authenticate(completion: #escaping ((Bool) -> ())){
//Create a context
let authenticationContext = LAContext()
var error:NSError?
//Check if device have Biometric sensor
let isValidSensor : Bool = authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
if isValidSensor {
//Device have BiometricSensor
//It Supports TouchID
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Touch / Face ID authentication",
reply: { [unowned self] (success, error) -> Void in
if(success) {
// Touch / Face ID recognized success here
completion(true)
} else {
//If not recognized then
if let error = error {
let strMessage = self.errorMessage(errorCode: error._code)
if strMessage != ""{
self.showAlertWithTitle(title: "Error", message: strMessage)
}
}
completion(false)
}
})
} else {
let strMessage = self.errorMessage(errorCode: (error?._code)!)
if strMessage != ""{
self.showAlertWithTitle(title: "Error", message: strMessage)
}
}
}
func errorMessage(errorCode:Int) -> String{
var strMessage = ""
switch errorCode {
case LAError.Code.authenticationFailed.rawValue:
strMessage = "Authentication Failed"
case LAError.Code.userCancel.rawValue:
strMessage = "User Cancel"
case LAError.Code.systemCancel.rawValue:
strMessage = "System Cancel"
case LAError.Code.passcodeNotSet.rawValue:
strMessage = "Please goto the Settings & Turn On Passcode"
case LAError.Code.touchIDNotAvailable.rawValue:
strMessage = "TouchI or FaceID DNot Available"
case LAError.Code.touchIDNotEnrolled.rawValue:
strMessage = "TouchID or FaceID Not Enrolled"
case LAError.Code.touchIDLockout.rawValue:
strMessage = "TouchID or FaceID Lockout Please goto the Settings & Turn On Passcode"
case LAError.Code.appCancel.rawValue:
strMessage = "App Cancel"
case LAError.Code.invalidContext.rawValue:
strMessage = "Invalid Context"
default:
strMessage = ""
}
return strMessage
}
func showAlertWithTitle( title:String, message:String ) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let actionOk = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(actionOk)
self.present(alert, animated: true, completion: nil)
}
Explanation:
'unowned' may only be applied to class and class-bound protocol types, not 'AuthenticateView'
First of all, you have AuthenticateView which is a struct. You can't do it class because whole Apple's SwiftUI idea is about structures. And because Struct is value type and not a Reference type, so no pointer as such. So you may not include code parts containing unowned self and weak self modifiers into struct AuthenticateView: View {}
Value of type 'AuthenticateView' has no member 'present'
present is a UIViewController's method. Here in SwiftUI you have no access to it. The alerts are being presented using the next style:
struct ContentView: View {
#State private var show = false
var body: some View {
Button(action: { self.show = true }) { Text("Click") }
.alert(isPresented: $showingAlert) {
Alert(title: Text("Title"),
message: Text("Message"),
dismissButton: .default(Text("Close")))
}
}
}
Solution:
For your case, I would create a class Handler subclass of ObservableObject for your logic and use the power of #ObservedObject, #Published and #State.
Rough example for understanding the concept:
import SwiftUI
struct ContentView: View {
#ObservedObject var handler = Handler()
var body: some View {
Button(action: { self.handler.toggleShowAlert() }) { Text("Click") }
.alert(isPresented: $handler.shouldShowAlert) {
Alert(title: Text(handler.someTitle),
message: Text(handler.someMessage),
dismissButton: .default(Text("Close")))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Handler: ObservableObject {
#Published var shouldShowAlert: Bool = false
#Published var someTitle = ""
#Published var someMessage = ""
func toggleShowAlert() {
shouldShowAlert.toggle()
someTitle = "ErrorTitle"
someMessage = "ErrorMessage"
}
}