Unable to display Alert from an Observable Object - swiftui

Scenario:
I want to be able to display an Alert() from an Observable Object, via #Published values (networkMessage & hasAlert).
This alert should be fired when missing a network connection.
via
Host View that displays the Alert() :
import SwiftUI
struct EndPointView: View {
#EnvironmentObject var settings: Settings
#ObservedObject var standardWeatherReport = StandardWeatherReport()
#ObservedObject var publishedWeatherReport = PublishedWeatherReport()
#ObservedObject var pepBoy = PepBoy()
#ObservedObject var postMan = PostMan()
var body: some View {
Form {
Text("Chosen One: \(settings.endpointSection)")
Text("Row Two")
Text("Row Three")
Text("Row Four")
}.onAppear {
self.acquireData()
}
.alert(isPresented: $standardWeatherReport.hasAlert, content: { () -> Alert in
Alert(title: Text(verbatim: standardWeatherReport.networkMessage!))
})
}
func acquireData() {
let chosenEndPoint = EndPoints(rawValue: settings.endpointSection)
switch chosenEndPoint {
case .standardWeather:
standardWeatherReport.doStandard()
case .publishedWeather:
publishedWeatherReport.doPublish()
case .postman:
postMan.doPublishPostMan()
case .publishpepboy:
pepBoy.doPublishPep()
case .none:
print("none")
}
}
}
Observable Object the initially fired off the Alert() via case .failure(let anError):
import Foundation
class PublishedWeatherReport: ObservableObject {
#Published var networkMessage: String?
#Published var hasAlert = false
#Published var weatherReport: String?
func doPublish() {
let url = EndPoint.weather.path()
var request = URLRequest(url: EndPoint.weather.path()!)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// Publisher:
let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: url!)
// the dataTaskPublisher output combination is (data: Data, response: URLResponse)
.map { $0.data }
.decode(type: Sample.self, decoder: decoder)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
// Subscriber:
sub = remoteDataPublisher // ...must assign to an iVar to keep alive.
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let anError):
self.networkMessage = anError.localizedDescription
self.hasAlert = true. // ... unable to host Alert().
}
}, receiveValue: { someValue in
print(".sink() received \(someValue)")
})
}
}
Question: Why am I not getting this alert() via
(isPresented: $standardWeatherReport.hasAlert)?

standardWeatherReport.hasAlert is not from the class PublishedWeatherReport.
where is the StandardWeatherReport class

I got two publishers mixed.
Hence the concerned alert wasn't functioning correctly.
Also I have multiple alert operators, each one per publisher.
This is a bad design: Only one (1) alert operator is allowed per host; otherwise the second .alert operator would override the previous operator.
So if one alert is active, a following inactive alert operator would immediately cancel it.

Related

SwiftUI Combine - How to test waiting for a publisher's async result

I am listening for changes of a publisher, then fetching some data asynchronously in my pipeline and updating the view with the result. However, I am unsure how to make this testable. How can I best wait until the expectation has been met?
View
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
NavigationView {
List(viewModel.results, id: \.self) {
Text($0)
}
.searchable(text: $viewModel.searchText)
}
}
}
ViewModel
final class ContentViewModel: ObservableObject {
#Published var searchText: String = ""
#Published var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
observeSearchText()
}
func observeSearchText() {
$searchText
.dropFirst()
.debounce(for: 0.8, scheduler: DispatchQueue.main)
.sink { _ in
Task {
await self.fetchResults()
}
}.store(in: &cancellables)
}
private func fetchResults() async {
do {
try await Task.sleep(nanoseconds: 1_000_000_000)
self.results = ["01", "02", "03"]
} catch {
//
}
}
}
Tests
class ContentViewTests: XCTestCase {
func testExample() {
// Given
let viewModel = ContentViewModel()
// When
viewModel.searchText = "123"
// Then (FAILS - Not waiting properly for result/update)
XCTAssertEqual(viewModel.results, ["01", "02", "03"])
}
}
Current Workaround
If I make fetchResults() available I can async/await which works for my unit and snapshot tests, but I was worried that:
It is bad practice to expose if it isn't to be called externally?
I'm not testing my publisher pipeline
func testExample_Workaround() async {
// Given
let viewModel = ContentViewModel()
// When
await viewModel.fetchResults()
// Then
XCTAssertEqual(viewModel.results, ["01", "02", "03"])
}
You need to wait asynchronously via expectation and check result via publisher.
Here is possible approach. Tested with Xcode 13.2 / iOS 15.2
private var cancelables = Set<AnyCancellable>()
func testContentViewModel() {
// Given
let viewModel = ContentViewModel()
let expect = expectation(description: "results")
viewModel.$results
.dropFirst() // << skip initial value !!
.sink {
XCTAssertEqual($0, ["01", "02", "03"])
expect.fulfill()
}
.store(in: &cancelables)
viewModel.searchText = "123"
wait(for: [expect], timeout: 3)
}

Calling a new view in a function SwiftUI

I am currently using an api to grab the definitions for a specific word that the user has entered, and the api returns multiple definitions. I want the user to be able to choose what exact definition they want to pair a word with. Since I am interacting with an api, it is in a function and I cannot return anything out of it. I want to grab all the definitions and then show a new view where the user can pick the appropriate definition. How can I go about doing this? I've thought of making an ObservableObject that just has an array as a work around, but that seems a bit excessive. I am new to SwiftUI, so I am unsure whether or not this would be possible. However, I think it would not be because I am not trying to return a view anywhere or using any of the built in things that accepts views.
EDIT: I made SaveArray an ObservableObject and now my problem is that the object is not being updated by my getDef function call. Within the function it is but it is not editing the actual class or at least that is what it is looking like, because on my next view I have a foreach going through the array and nothing is displayed because it is empty. I am not sure whether that is because the sheet is being brought up before the getDef function is done executing.
struct AddWord: View {
#ObservedObject var book: Book
#ObservedObject var currentArray = SaveArray()
#State var addingDefinition = false
#State var word = ""
#State var definition = ""
#State var allDefinitions: [String] = []
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form {
TextField("Word: ", text: $word)
}
.navigationBarTitle("Add word")
.navigationBarItems(trailing: Button("Add") {
if self.word != "" {
book.words.append(self.word)
getDef(self.word, book, currentArray)
addingDefinition = true
self.presentationMode.wrappedValue.dismiss()
}
}).sheet(isPresented: $addingDefinition) {
PickDefinition(definitions: currentArray, book: book, word: self.word)
}
}
}
func getDef(_ word: String, _ book: Book, _ definitionsArray: SaveArray) {
let request = NSMutableURLRequest(url: NSURL(string: "https://wordsapiv1.p.rapidapi.com/words/\(word)")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
let dict = dictionary?["results"] as? [Any]
definitionsArray.currentArray = createArray((dict!))
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
func createArray(_ array: [Any]) -> [String] {
//Get all the definitions given from the api and put it into a string array so you can display it for user to select the correct definiton for their context
var definitions = [String]()
for object in array {
let dictDef = object as? [String: Any]
definitions.append(dictDef?["definition"] as! String)
}
return definitions
}
}
struct AddWord_Previews: PreviewProvider {
static var previews: some View {
AddWord(book: Book())
}
}
struct PickDefinition: View {
#ObservedObject var definitions: SaveArray
var book: Book
var word: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
ForEach(0 ..< definitions.currentArray.count) { index in
Button("\(self.definitions.currentArray[index])", action: {
print("hello")
DB_Manager().addWords(name: self.book.name, word: self.word, definition: self.definitions.currentArray[index])
book.definitions.append(self.definitions.currentArray[index])
self.presentationMode.wrappedValue.dismiss()
})
}
}
.navigationTitle("Choose")
}
}
}
struct PickDefinition_Previews: PreviewProvider {
static var previews: some View {
PickDefinition(definitions: SaveArray(), book: Book(), word: "")
}
}
If you can post more of your code, I can provide a fully working example (e.g. the sample JSON and the views/classes you have built). But for now, I am working with what you provided. I hope the below will help you see just how ObservableObject works.
#Published var dict = [String]() //If the api returns a list of strings, you can make this of type string - I do not have a sample of the JSON so I cannot be sure. If you can provide a sample of the JSON I can better define the way this should work.
var body: some View {
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
dict = dictionary?["results"] as? [Any] //note that here we are assigning the results of the api call to the Published variable, which our StateObject variable in ContentView is listening to!
var allDef = createArray((dict!))
//No longer need to pass this data forward (as you have below) since we are publishing the information!
//Pass the array to the new view where the user will select the one they want
PickDefinition(definitions: allDef, book: self.book, word: self.word)
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
}
struct ContentView: View {
//StateObject receives updates sent by the Published variable
#StateObject var dictArray = Api()
var body: some View {
NavigationView {
List {
ForEach(dictArray.dict.indices, id: \.self) { index in
Text(dictArray.dict[index])
}
}
}
}
}

How do I fix a value member not found error in iOS foundation with swift while working with notifications?

I'm trying to experiment with iOS notifications, so I tried making a swiftUI view that would send a notification, basing it off a tutorial Found Here. I have wrote this so far:
import SwiftUI
import Foundation
class LocalNotificationManager: ObservableObject {
var notifications = [Notification]()
init() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound])
{ granted, error in
if granted == true && error == nil {
print("yay")
} else {
print("boo")
}
}
func sendNotification(title: String, subtitle: String?, body: String, LaunchIn: Double) {
let content = UNMutableNotificationContent()
content.title = title
if let subtitle = subtitle {
content.subtitle = subtitle
}
content.body = body
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: LaunchIn, repeats: false)
let request = UNNotificationRequest(identifier: "demoNotification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
}
}
struct Screen: View {
#ObservedObject var NotificationManager = LocalNotificationManager()
var body: some View {
Button(action: {
self.notificationManager.sendNotification(title: "It worked", subtitle: ":D", body: "If you see this text, launching the local notification worked!", launchIn: 5)
}) {
Text("Test Notification")
}
}
}
struct Screen_Previews: PreviewProvider {
static var previews: some View {
Screen()
}
}
Its this line where I get problems:
self.notificationManager.sendNotification(title: "It worked", subtitle: ":D", body: "If you see this text, launching the local notification worked!", launchIn: 5)
I get an error that says:
Value of type 'Screen' has no member 'notificationManager'
Change
#ObservedObject var NotificationManager = LocalNotificationManager()
To
#StateObject var notificationManager = LocalNotificationManager()
Variables are case-sensitive and should start with a lowercase
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
https://swift.org/documentation/api-design-guidelines/

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.