How to pass data between views in swiftui? - swift

I have a view, in which I want the user to enter data. Then I want the user, to be able to press a button which directs the user to another view. And depending on the value, which the user entered previously I want a link to be changed. The link is at top-level because I need it more than once. So for example if the user enters "89", I want the link to be "https://www.example.com/89".
this is the code of the first view:
class lClass: ObservableObject {
#Published var longitude: String = ""
}
struct Data: View {
#State var showGraph = false
#ObservedObject var longitude2 = lClass()
var body: some View {
Button(action: {
self.showGraph.toggle()
}) {
Text("Show Graph")
}.sheet(isPresented: $showGraph){
GraphView()
}
if showGraph {
GraphView()
}
else {
HStack {
TextField("Longitude:", text: $longitude2.longitude)
}.frame(width: 400, height: 100, alignment: .center)
}
}
}
And this is the shortened code of the second view:
var month = 1
var day = 1
var year = 2020
var latitude = 59.911232
var longitude = 10.757933
var offset = "1.0"
class test {
#ObservedObject var longitude2 = lClass()
}
class test2 {
var car = test.init().longitude2
}
private let url = URL(string: "https://midcdmz.nrel.gov/apps/spa.pl?syear=\(year)&smonth=\(month)&sday=\(day)&eyear=\(year)&emonth=\(month)&eday=\(day)&otype=0&step=60&stepunit=1&hr=12&min=0&sec=0&latitude=\(latitude)&longitude=\(longitude)&timezone=\(offset)&elev=53&press=835&temp=10&dut1=0.0&deltat=64.797&azmrot=180&slope=0&refract=0.5667&field=0")
private func loadData(from url: URL?) -> String {
guard let url = url else {
return "nil"
}
let html = try! String(contentsOf: url, encoding: String.Encoding.utf8)
return html
}
let html = loadData(from: url)
private func dbl1() -> Double {
let leftSideOfTheValue = "0:00:00,"
let rightSideOfTheValue = "\(month)/\(day)/\(year),1:00:00,"
guard let leftRange = html.range(of: leftSideOfTheValue) else {
print("cant find left range")
return 0
}
guard let rightRange = html.range(of: rightSideOfTheValue) else {
print("cant find right range")
return 0
}
let rangeOfTheValue = leftRange.upperBound..<rightRange.lowerBound
return Double(html[rangeOfTheValue].dropLast()) ?? 90
}
struct elevationGraph: View {
var body: some View {
Text(String(dbl1())
}
}
In the end, I want the user to be able to select the vars, month, day, year, longitude, latitude, offset in the first view. And then manipulate the url so that it uses the data entered by the user.

So the question is how do I update and share information across the application's views.
One way to do this is through the use of one or more common objects that do not get destroyed when the view is updated. To do this, the code either needs to create the object outside of the View structure. Or to use #StateObject in the View where the object is being instantiated.
Once created, the instance can then either be passed as a parameter to child views as an #ObservedObject parameter. Or if it used in many places and levels in the the app, it can be placed in the environment and extracted through the use of the #EnvironmentObject to avoid "prop drilling"
These two StackOverflow links should help clarify:
#ObservedObject vs #StateObject
#EnvironmentObject
Good luck.

Related

Unable to pass down returned value from now view to another

I have a view that returns a value called 'idd' in the form of a string: enter image description here
Now I have a view called ListView where there is a navigationLink that essentially needs that returned 'idd' value to be passed to this view called NoteView which is that navigationLink's destination. However I'm not able to pass that value down to NoteView. When I use:
self.idd = datamanager.addnote()
to define idd in Listview(desc), it just says that ListView has no member idd. I've been struggling with this for days now, I'd appreciate if someone could give me their two cents! Thanks a ton.
EDITS: Described the code much better
List View: enter image description here
Here is how addNote returns idd enter image description here
Here is how NoteView that accepts the idd value when it's supposed to be passed down from ListView Navigation Link enter image description here
Here is List View that is supposed to take the idd value
struct ListView: View {
#StateObject var dataManager = DataManager()
#State var isPresented = false
var body: some View {
NavigationView {
ZStack{
List(dataManager.notes) { note in
NavigationLink(destination: NoteView(newNote: note.content, idd: note.id)) {
EmptyView()
Text(note.content)}.buttonStyle(PlainButtonStyle())
.frame(height: 0)
.navigationTitle("Notes")
.navigationBarItems(
trailing: Button (action: {},
label: {
NavigationLink(destination: NoteView(newNote: "", idd: "") )
{Image(systemName: "plus")}
.simultaneousGesture(TapGesture().onEnded{
dataManager.addNote()
})}
))
.listStyle(.plain)
.buttonStyle(PlainButtonStyle())
}
Here is addNote()
class DataManager : ObservableObject {
#Published var notes: [Note] = []
#Published var idd = ""
func addNote()-> String{
let user = Auth.auth().currentUser
let uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
var ref: DocumentReference? = nil
ref = db.collection("userslist").document(uid).collection("actualnotes").addDocument(data: ["content":"New Note"])
{error in
if let error = error{
print(error.localizedDescription)
}
else {
print("Document added with ID: \(ref!.documentID)")
}
}
let idd = ref!.documentID
return idd
}

SwiftUI - Show the data fetched from Firebase in view?

Firebase I am trying to show data taken from Firestore in my SwiftUI view but I have a problem. I have no problem with pulling data from Firebase. But I cannot show the data as I want. I work with MVVM architecture.
My model is like this:
struct UserProfileModel: Identifiable {
#DocumentID var id : String?
var username : String
var uidFromFirebase : String
var firstName : String
var lastName : String
var email : String
}
ViewModel:
class UserProfileViewModel: ObservableObject {
#Published var user : [UserProfileModel] = []
private var db = Firestore.firestore()
func data(){
db.collection("Users").whereField("uuidFromFirebase", isEqualTo: Auth.auth().currentUser!.uid).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No Documents")
return
}
self.user = documents.compactMap { queryDocumentSnapshot -> UserProfileModel? in
return try? queryDocumentSnapshot.data(as: UserProfileModel.self)
}
}
}
}
View:
struct MainView: View {
#ObservedObject private var viewModel = UserProfileViewModel()
var body: some View { // -> Error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
VStack{
Text(viewModel.user.username) // -> I want to do this but XCode is giving an error.
// This works but I don't want to do it this way.
List(viewModel.user) { user in
VStack{
Text(user.username)
Text(user.firstName)
Text(user.lastName)
Text(user.email)
Text(user.uidFromFirebase)
}
}
}
}
}
In the videos and articles titled "SwiftUI fetch data from Firebase" that I watched and read, I have always narrated on List and ForEach. But I want to use the data wherever. I shared all my code with you. I want to learn how I can do this.
Looks to me like you really just want to have one user that you're pulling the data for, but you've set up your UserProfileViewModel with an array of users ([UserProfileModel]). There are a number of ways that you could take care of this. Here's one (check the code for inline comments about what is going on):
class UserProfileViewModel: ObservableObject {
#Published var user : UserProfileModel? = nil // now an optional
private var db = Firestore.firestore()
func data(){
db.collection("Users").whereField("uuidFromFirebase", isEqualTo: Auth.auth().currentUser!.uid).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No Documents")
return
}
self.user = documents.compactMap { queryDocumentSnapshot -> UserProfileModel? in
return try? queryDocumentSnapshot.data(as: UserProfileModel.self)
}.first // Take the first document (since there probably should be only one anyway)
}
}
}
struct MainView: View {
#ObservedObject private var viewModel = UserProfileViewModel()
var body: some View {
VStack {
if let user = viewModel.user { //only display data if the user isn't nil
Text(user.username)
Text(user.firstName)
Text(user.lastName)
Text(user.email)
Text(user.uidFromFirebase)
}
}
}
}
I'd say a more traditional way of handling this might be to store your user profile document Users/uid/ -- that way you can just user document(uid) to find it rather than the whereField query.

SwiftUI published variable not updating

I'm trying to set a published variable named allCountries. In my DispatchQueue, it's being set and is actually printing out the correct information to the console. In my content view, nothing is in the array. In the init function on the content view, I'm calling the the function that will fetch the data and set the variable but when I print to the console, the variable is blank. Can someone tell me what I'm doing wrong?
Here is where I'm fetching the data
class GetCountries: ObservableObject {
#Published var allCountries = [Countries]()
func getAllPosts() {
guard let url = URL(string: "apiURL")
else {
fatalError("URL does not work!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
do {
if let data = data {
let posts = try JSONDecoder().decode([Countries].self, from: data)
DispatchQueue.main.async {
self.allCountries = posts
print(self.allCountries)
}
} else {
DispatchQueue.main.async {
print("didn't work")
}
}
}
catch {
DispatchQueue.main.async {
print(error.localizedDescription)
}
}
}.resume()
}
}
Here's my content view
struct ContentView: View {
var countries = ["Select Your Country", "Canada", "Mexico", "United States"]
#ObservedObject var countries2 = GetCountries()
#State private var selectedCountries = 0
init() {
GetCountries().getAllPosts()
print(countries2.allCountries)
}
var body: some View {
NavigationView {
ZStack {
VStack {
Text("\(countries2.allCountries.count)")}}}
The problem is happening in the init of your ContentView. You're calling the TYPE's function getAllPosts, but then you're trying to access the data of a new INSTANCE of that Type countries2. Your countries2.allCountries instance property really has no relationship to the GetCountries.allCountries type property.
Does that even compile? Usually you need to define a class's functions and properties as static if you want to call them on the Type itself and not on an instance of it.
Maybe it's weirdness because you're trying to do this in the ContentView's init, rather than after it has initialized.
There's probably two things you need to do to fix this.
Step one would be to let your custom initializer actually initialize countries2.
And step two would be to call countries2.getAllPosts within that custom initializer, which will populate the instance property you're trying to access in countries2.allCountries.
Try this:
struct ContentView: View {
var countries = ["Select Your Country", "Canada", "Mexico", "United States"]
// You'll initialize this in the custom init
#ObservedObject var countries2: GetCountries
#State private var selectedCountries = 0
init() {
countries2 = GetCountries()
countries2.getAllPosts()
print(countries2.allCountries)
}
var body: some View {
NavigationView {
ZStack {
VStack {
Text("\(countries2.allCountries.count)")}}}
But I'm not sure if you're going to run into problems with the async call within the ContentView initializer. It might be better practice to move that into a function which is called when the view is loaded, rather than when the SwiftUI struct is initialized.
To do that you move your networking calls into a function of ContentView (instead of its init), then use an .onAppear modifier to your view, which will call a function when the NavigationView appears. (Now that we're not using a custom initializer, we'll need to go back to initializing the countries2 property.)
struct ContentView: View {
var countries = ["Select Your Country", "Canada", "Mexico", "United States"]
// We're initializing countries2 again
#ObservedObject var countries2 = GetCountries()
#State private var selectedCountries = 0
// This is the function that will get your country data
func getTheCountries() {
countries2.getAllPosts()
print(countries2.allCountries)
}
var body: some View {
NavigationView {
ZStack {
VStack {
Text("\(countries2.allCountries.count)")
}
}
}
.onAppear(perform: getTheCountries) // Execute this when the NavigationView appears
}
}
Note with this approach. Depending on your UI architecture, every time NavigationView appears, it will call getTheCountries. If you make NavigationLink calls to new views, then it may get reloaded loaded if you go back to this first page. But you can easily add a check within getTheCountries to see if the work is actually required.
It should be used one same instance to initiate request & read results, so here is fixed variant:
struct ContentView: View {
var countries = ["Select Your Country", "Canada", "Mexico", "United States"]
#ObservedObject var countries2 = GetCountries() // member
#State private var selectedCountries = 0
init() {
// GetCountries().getAllPosts() // [x] local variable
// print(countries2.allCountries)
}
var body: some View {
NavigationView {
ZStack {
VStack {
Text("\(countries2.allCountries.count)")
}
}
}
.onAppear { self.countries2.getAllPosts() } // << here !!
}
}

String contents not being changed

This is my main view where I create an object of getDepthData() that holds a string variable that I want to update when the user click the button below. But it never gets changed after clicking the button
import SwiftUI
struct InDepthView: View {
#State var showList = false
#State var pickerSelectedItem = 1
#ObservedObject var data = getDepthData()
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.data.whichCountry = "usa"
print(" indepthview "+self.data.whichCountry)
}) {
Text("change value")
}
}
}
}
Here is my class where I hold a string variable to keep track of the country they are viewing. But when every I try to modify the whichCountry variable it doesn't get changed
class getDepthData: ObservableObject {
#Published var data : Specific!
#Published var countries : HistoricalSpecific!
#State var whichCountry: String = "italy"
init() {
updateData()
}
func updateData() {
let url = "https://corona.lmao.ninja/v2/countries/"
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url+"\(self.whichCountry)")!) { (data, _, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
let json = try! JSONDecoder().decode(Specific.self, from: data!)
DispatchQueue.main.async {
self.data = json
}
}.resume()
}
}
Any help would be greatly appreciated!
You need to define the whichCountry variable as #Published to apply changes on it
#Published var whichCountry: String = "italy"
You need to mark whichCountry as a #Published variable so SwiftUI publishes a event when this property have been changed. This causes the body property to reload
#Published var whichCountry: String = "italy"
By the way it is a convention to write the first letter of your class capitalized:
class GetDepthData: ObservableObject { }
As the others have mentioned, you need to define the whichCountry variable as #Published to apply changes to it. In addition you probably want to update your data because whichCountry has changed. So try this:
#Published var whichCountry: String = "italy" {
didSet {
self.updateData()
}
}

Display query values within view's body

What I want to do: I want to display the user's name on the Profile view after a user logs into the app. Currently I have implemented a login using Firebase Auth. When users create an account, it creates a record in the "Users" collection in Firestore that records First and Last Name and email address.
What I've Tried: On the Profile view, I currently have a function "getUser" that checks if the user is logged in, and then matches the user's Firebase Auth email address and matches it to the record in Firestore. This is working because I've checked what info the query is returning by what's logged in the console. However, I'm at a lost how to get the "fName" information and displaying it within the view's body.
Here's screenshots of the database structure and my current code.
import SwiftUI
import Firebase
import Combine
import FirebaseFirestore
struct ProfileView: View {
#EnvironmentObject var session: SessionStore
let db = Firestore.firestore()
func getUser() {
session.listen()
let query = db.collection("users").whereField("email", isEqualTo: session.session!.email!)
query.getDocuments { (QuerySnapshot, err) in
if let docs = QuerySnapshot?.documents {
for docSnapshot in docs {
print (docSnapshot.data())
}
}
}
}
var body: some View {
Group {
if (session.session != nil) {
NavigationView {
VStack {
Text("Welcome back \(session.session!.email ?? "user")")
Spacer()
Button(action: session.signOut) {
Text("Sign Out")
}.padding(.bottom, 60)
}
} // end NavigationView
} else {
AuthView()
}
}.onAppear(perform: getUser)
}
}
I guess you should save the values returned from Firestore with UserDefaults
import UIKit
//your get user data logic here
let fName:String = <the first name retrieved from firestore>
// Get the standard UserDefaults as "defaults"
let defaults = UserDefaults.standard
// Save the String to the standard UserDefaults under the key, "first_name"
defaults.set(fName, forKey: "first_name")
And in your profile page you should retrieve this value
// you should have defaults initialized here
fnameToDisplay = defaults.string(forKey: "first_name")
Hello I prefer to make a ViewModel - NetworkManager class that comfort ObservableObject and inside of it you can get and fetch data and when they finish loading they will update the view, Let me show you an example how it works:
as you can see I created a NetworkManager extends ObservableObject (cause I want to get notified when the user finish loading) inside of it you can see that var user is annotated with #Published that make the user observed and when a new data set will notify the View to update. also you can see that I load the data on init so the fist time this class initialized it will call the fetchData() func
class NetworkManager: ObservableObject {
let url: String = "https://jsonplaceholder.typicode.com/todos/1"
var objectWillChange = PassthroughSubject<NetworkManager, Never>()
init() {
fetchData()
}
#Published var user: User? {
didSet {
objectWillChange.send(self)
print("set user")
}
}
func fetchData() {
guard let url = URL(string: url) else {return}
print("fetch data")
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {return}
print("no error")
guard let data = data else {return}
print("data is valid")
let user = try! JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.user = user
}
}.resume()
}
}
ContentView is where I'm gonna place the user data or info I declared #ObservedObject var networkManager cause it extend ObservableObject and I place the views and pass User
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
VStack {
DetailsView(user: networkManager.user)
}
}
}
struct DetailsView: View {
var user: User?
var body: some View {
VStack {
Text("id: \(user?.id ?? 0)")
Text("UserID: \(user?.userId ?? 0 )")
Text("title: \(user?.title ?? "Empty")")
}
}
}
class User: Decodable, ObservableObject {
var userId: Int = 0
var id: Int = 0
var title: String = ""
var completed: Bool = false
}
PS: I didn't make objects unwrapped correctly please consider that so
you not to have any nil exception