How to show alert in ContentView based on error in class - swiftui

I have class handling some CoreData stuff
class GenreData: NSObject {
var genreID: Int = 0
var genreName: String = ""
#Published var isError = false
func getGenreName() -> String {
let request = NSFetchRequest<NSManagedObject>(entityName: "Genres")
request.predicate = NSPredicate(format: "genID == \(genreID)")
var genre = [NSManagedObject]()
do {
try genre = context.fetch(request)
} catch let error {
isError = true
print(error.localizedDescription)
}
return genre.first!.value(forKey: "genName") as! String
}
}
and I'd like to know how to initiate alert in my ContentView in case of error occured in the class.
.alert(isPresented: $isError, content: {
Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text(alertButton1)))
})
.alert(isPresented: $objGenre.dataError, content: {
Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text(alertButton1)))
})
.alert(isPresented: $deleteAlert, content: {
Alert(title: Text(alertTitle), message: Text(alertMessage), primaryButton: .destructive(Text(alertButton1)) {
objGenre.deleteGenre()
objGenre.genreID = 0
selectedGenreName = ""
}, secondaryButton: .cancel(Text(alertButton2)))
})
I set class as #ObservableObject objGenre in ContentView and declared #Published var isError in the class but alert is not shown even if isError is set to true.

Your GenreData class needs to conform to the ObservableObject protocol and you need a strong reference of it in your view with the property wrapper of #ObservedObject. That will enable the published property to update view state and display the alert. This article is a good place to start if you want to learn about alerts in SwiftUI..
https://medium.com/better-programming/alerts-in-swiftui-a714a19a547e

Related

SwiftUI Combine How to update textfield

I'm learning Combine and how it can update a value using publishers. Currently I have created a variable that updates itself when validation fails.
var nameError: AnyPublisher<String, Never> {
$name
.dropFirst()
.debounce(for: 0.2, scheduler: RunLoop.main)
.removeDuplicates()
.map {
if $0.isEmpty || !self.isValidName() {
return "Name not valid"
}
return "Name"
}
.eraseToAnyPublisher()
}
I want to attach this to a Text() label so that it updates. Before Combine I would have an var error: String that I would check against. But now I get the error Cannot convert value of type 'AnyPublisher<String, Never>' to expected argument type 'String'
How do I convert a var error: String message to receive an AnyPublisher<String, Never>?
Below is a Playground that I think implements what you are asking.
The upshot is that you subscribe to your publisher in onAppear and keep the subscription in the state of your view. When a new value is published you update the state holding the caption.
import UIKit
import SwiftUI
import Combine
import PlaygroundSupport
class ModelObject : ObservableObject {
#Published var name: String = ""
var nameError: AnyPublisher<String, Never> {
$name
.dropFirst()
.debounce(for: 0.2, scheduler: RunLoop.main)
.print()
.removeDuplicates()
.map {
if $0.isEmpty || !self.isValidName() {
return "Name not valid"
}
return ""
}
.eraseToAnyPublisher()
}
func isValidName() -> Bool {
return name.caseInsensitiveCompare("Scott") != .orderedSame
}
}
struct TextControl : View {
#StateObject var viewModel = ModelObject()
#State var caption : String = ""
#State var subscription : AnyCancellable?
var body : some View {
VStack(alignment: .leading) {
TextField("Entry", text: $viewModel.name)
Text(caption)
.font(.caption)
.foregroundColor(.red)
}
.padding()
.frame(width: 320, height: 240)
.onAppear {
self.subscription = viewModel.nameError.sink { self.caption = $0}
}
}
}
let viewController = UIHostingController(rootView: TextControl())
PlaygroundSupport.PlaygroundPage.current.liveView = viewController

How to change #State var from Swift class

I have SwiftUI ContentView struct from which I call function in standard Swift class. This function may throw an error which I`d like to show via Alert in ContentView. Appearance of Alert is controlled by Bool #State var declared in ContentView.
I was trying to use #Binding property wrapper in the function but it is obviously not correct. Should I rather use ObservableObject or what is the best approach?
Thanks.
Fragment of ContentView with Alert
HStack {
Button("Load data...", action: {
let panel = NSOpenPanel()
panel.title = "Select CSV formatted data file"
panel.canChooseFiles = true
panel.allowedFileTypes = ["csv"]
panel.allowsMultipleSelection = false
panel.begin(completionHandler: {result in
if result == .OK {
getDataset(fromFileURL: panel.url!, withHeaderLine: headerLine)
}
})
})
.padding()
.alert(isPresented: $isError, content: {
Alert(title: Text("Error"), message: Text(errorText), dismissButton: .default(Text("OK")))
})
Toggle("With header line", isOn: $headerLine)
}.toggleStyle(SwitchToggleStyle())
}
Fragment of called function which can throw error
do {
var fromRow = 0
let fileContent = try String(contentsOf: fromFileURL)
let rows = fileContent.components(separatedBy: "\n")
if withHeaderLine { fromRow = 1 }
for i in fromRow...rows.count - 1 {
let columns = rows[i].components(separatedBy: ",")
guard let xValue = Double(columns[0]) else {
throw myError.conversionFailed
}
guard let yValue = Double(columns[1]) else {
throw myError.conversionFailed
}
myDataset.append(Dataset(x: xValue, y: yValue))
}
} catch myError.conversionFailed {
errorText = "Value conversion to Double failed."
isError.toggle()
} catch let error {
errorText = error.localizedDescription
isError.toggle()
}
}
I would suggest creating a ViewModel for that View. Inside that ViewModel you create the two PublishedValues for the errorText and isError. Then you can the function inside ViewModel and update Published value. ViewModel would look like this and then update your other View accordingly.
class ContentViewModel : ObservableObject {
#Published var isError : Bool = false
#Published var errorText : String = ""
func getDataset() {
//Here you call your function and return the result or call it directly inside here
errorText = "Value conversion to Double failed." //<< here you can change published values
isError.toggle()
}
}
Create ViewModel and map to their States
struct ContentView : View {
#ObservedObject var viewModel : ContentViewModel = ContentViewModel()
#State var headerLine : Bool = false
var body : some View {
HStack {
Button("Load data...", action: {
let panel = NSOpenPanel()
panel.title = "Select CSV formatted data file"
panel.canChooseFiles = true
panel.allowedFileTypes = ["csv", "png"]
panel.allowsMultipleSelection = false
panel.begin(completionHandler: {result in
if result == .OK {
viewModel.getDataset()
}
})
})
.padding()
.alert(isPresented: $viewModel.isError, content: {
Alert(title: Text("Error"), message: Text(viewModel.errorText), dismissButton: .default(Text("OK")))
})
Toggle("With header line", isOn: $headerLine)
.toggleStyle(SwitchToggleStyle())
}
}
}
If you still outsourced your function into another view, just return the error String from that function or use closures.
Here is a demo of possible approach (with some simulation of async call, if it matters)
Tested with Xcode 12.1 / iOS 14.1
class DemoClass {
func simulate(isError: Binding<Bool>) {
DispatchQueue.global(qos: .background).async {
sleep(1)
DispatchQueue.main.async {
isError.wrappedValue.toggle()
}
}
}
}
struct ContentView: View {
let demo = DemoClass()
#State private var isError = false
var body: some View {
VStack {
Button("Demo") { demo.simulate(isError: $isError) }
}
.alert(isPresented: $isError, content: {
Alert(title: Text("Error"), message: Text("errorText"), dismissButton: .default(Text("OK")))
})
}
}

SwiftUI – Alert is only showing once

I have a strange problem with the SwiftUI Alert view. In an ObservableObject, I do some network requests and in case of a error I will show a alert. This is my simplified model:
class MyModel: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
#Published var isError: Bool = false
public func network() {
Service.call() {
self.isError = true
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
}
Service.call is a dummy for my network request. My view looks like:
struct MyView: View {
#ObservedObject var model: MyModel
var body: some View {
…
.alert(isPresented: self.$model.isError) {
print("Error Alert")
return Alert(title: Text("Alert"))
}
}
}
On the first call, everything works and the alert is shown. For all further calls,print("Error Alert") will be executed and Error Alert appears in the console, but the alert is not shown.
Does anyone have any idea why Alert is only shown once?
Try to use instead (there is already default publisher for #Published properties)
class MyModel: ObservableObject {
#Published var isError: Bool = false
public func network() {
Service.call() {
DispatchQueue.main.async {
self.isError = true // << !!! important place to call
}
}
}
}

Simplest way to pass #Published data to a Textfield()?

Scenario:
I'm using an Observable Class to acquire data from the network.
In this case some elementary weather data.
Problem:
I don't know how to display this data in the calling View.
For the time-being, I merely am trying to populate a Textfield (and worry about more-eleborate layout later).
I get the following:
.../StandardWeatherView.swift:22:13: Cannot invoke initializer for
type 'TextField<_>' with an argument list of type '(Text, text:
Sample?)'
Here's is my calling View which is the receiver of #ObservedObject data:
import SwiftUI
struct StandardWeatherView: View {
#EnvironmentObject var settings: Settings
#ObservedObject var standardWeatherReportLoader = StandardWeatherReportLoader()
init() {
self.standardWeatherReportLoader.doStandard()
}
var body: some View {
ZStack {
Color("FernGreen").edgesIgnoringSafeArea(.all)
TextField(Text("Weather Data"), text: standardWeatherReportLoader.weatherReport)
}
}
}
struct StandardWeatherView_Previews: PreviewProvider {
static var previews: some View {
StandardWeatherView()
}
}
Here's the publisher, acquiring data:
import Foundation
class StandardWeatherReportLoader: ObservableObject {
#Published var networkMessage: String?
#Published var hasAlert = false
#Published var weatherReport: Sample?
#Published var hasReport = false
func doStandard() {
let url = EndPoint.weather.path()
var request = URLRequest(url: EndPoint.weather.path()!)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: url!) { (data: Data?, _: URLResponse?, error: Error?) -> Void in
DispatchQueue.main.async {
guard error == nil else {
self.networkMessage = error?.localizedDescription
self.hasAlert = true
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Sample.self, from: data!)
self.weatherReport = result
self.hasReport = true
print("\n Standard Weather ----------------")
print(#function, "line: ", #line, "Result: ",result)
print("\n")
} catch let error as NSError {
print(error)
}
}
}
task.resume()
}
}
What's the simplest way of passing a string of data to the View via #Published var?
Log:
Standard Weather ---------------- doStandard() line: 38 Result:
Sample(coord: DataTaskPubTab.Coord(lon: -0.13, lat: 51.51), weather:
[DataTaskPubTab.Weather(id: 300, main: "Drizzle", description: "light
intensity drizzle")], base: "stations", main:
DataTaskPubTab.Main(temp: 280.32, pressure: 1012, humidity: 81,
tempMin: 279.15, tempMax: 281.15), visibility: 10000, wind:
DataTaskPubTab.Wind(speed: 4.1, deg: 80), clouds:
DataTaskPubTab.Clouds(all: 90), dt: 1485789600.0, id: 2643743, name:
"London")
But I'm getting nil at the TextField:
(lldb) po standardWeatherReportLoader.weatherReport nil
One option is to set a binding within your body to track whenever the TextField has updated. From within this binding, you can then edit your Published variable as you wish:
#ObservedObject var reportLoader = StandardWeatherReportLoader()
var body: some View {
// Binding to detect when TextField changes
let textBinding = Binding<String>(get: {
self.reportLoader.networkMessage
}, set: {
self.reportLoader.networkMessage = $0
})
// Return view containing the text field
return VStack {
TextField("Enter the Network Message", text: textBinding)
}
}
Edit: Also in your original post, you were passing an object of optional type Sample into the TextField which was expecting a binding String type which could cause some issues.

How to bind a Bool value(that I got from server) to hide/show an Alert in SwiftUI?

I've a simple Login app. enter username and password and click login button, i will get response from server which contains a Bool. if the Bool is true then go to next page, else show an Alert with error message.
struct ContentView : View {
#State var username: String = ""
#State var password: String = ""
#ObjectBinding var loginVM : LoginViewModel = LoginViewModel()
var body: some View {
NavigationView {
VStack {
TextField($username, placeholder: Text("Username")
TextField($password, placeholder: Text("Password")
Button(action: {
let params : [String:String] =
["username":self.username,
"password":self.password]
self.loginVM.doLogin(params:params)
}) {
Text("Login")
}
.alert(isPresented: $loginVM.isLogin) {
Alert(title: Text("Login Error"), message:
Text(loginVM.errorMessage),
dismissButton: .default(Text("Ok")))
}
}
}
}
//LoginViewModel:
class LoginViewModel : BindableObject {
let willChange = PassthroughSubject<Void,Never>()
var isLogin : Bool = false { willSet { willChange.send() } }
var errorMessage = "" { willSet { willChange.send() } }
func doLogin(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
if myresponse.login {
self.isLogin = true // this will fire willChange.send()
self.errorMessage = ""
} else {
self.isLogin = false
self.errorMessage = myresponse.errorMessage
}
}
}
}
//and my login response is:
{ "login" : true, "error": "" } OR
{ "login" : false, "error": "Password Incorrect"}
THE PROBLEM IS: if login is TRUE (in the response)then -> in the ContentView '$loginVM.isLogin' becomes TRUE and Alert will Show.
if login is FALSE then -> $loginVM.isLogin becomes FALSE and Alert will Not Show. I just want the OPPOSITE to happen. Means i want to show the alert only if the login is FALSE.
Also If the login is true, I want to go to next page(how to do that?), else show the errorMessage in the Alert.
Let's see :
You could add an additional property to your view model called isError - and make it true when there is an error condition in your model:
.alert(isPresented: $loginVM.isError) { ... }
Showing different things if loginVM.isLogin is true or false is even simpler:
if loginVM.isLogin {
// Showing the appropriate content for a logged in user
Text("Login succesful")
} else {
// showing the login form
TextField($username, placeholder: Text("Username")
TextField($password, placeholder: Text("Password")
Button(...)
}