SwiftUI Async data fetch in onAppear - swift

I have class getDataFromDatabase which has func readData() thats read data from Firebase.
class getDataFromDatabase : ObservableObject {
var arrayWithQuantity = [Int]()
var arrayWithTime = [Double]()
func readData(completion: #escaping(_ getArray: Array<Int>?,_ getArray: Array<Double>?) -> Void) {
let db = Firestore.firestore()
db.collection("amounts").getDocuments { (querySnapshot, err) in
if let e = err{
print("There's any errors: \(e)")
}
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in querySnapshot!.documents{
let quantityFromDb = i.get("amount") as! Int
let timeFromDb = i.get("averageTimeRecognition") as! Double
self.arrayWithQuantity.append(quantityFromDb)
self.arrayWithTime.append(timeFromDb)
}
completion(self.arrayWithQuantity, self.arrayWithTime)
}
}
}
I use func readData() in onAppear:
struct CheckDatabaseView: View {
#State private var quantityFromDatabase: Array<Int> = []
#State private var timeFromDatabase: Array<Double> = []
#State private var flowersName: Array<String> = ["Carnation", "Daisy", "Hyacinth", "Iris", "Magnolia", "Orchid", "Poppy", "Rose", "Sunflower", "Tulip"]
#State private var isReady: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false){
ZStack(alignment: .top){
VStack(spacing: 40){
Text("Hello, world!")
// BarView(value: CGFloat(timeFromDatabase[0]), name: flowersName[0])
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
}
.navigationBarTitle(Text("Your datas in database").foregroundColor(.blue), displayMode: .inline)
.onAppear{
let gd = getDataFromDatabase()
gd.readData { (quantity, time) in
self.quantityFromDatabase = quantity!
self.timeFromDatabase = time!
}
}
}
}
I cannot use values self.quantityFromDatabase and self.timeFromDatabase because are empty. I know the problem is with the asynchronous retrieval of data. I've tried with DispatchQueue.main.async, but I still not get these values. How is the other method to get it? I need this values, because I want to draw charts in VStack (the comment line there).
EDIT
As #Rexhin Hoxha wrote below, i modified the code but i am not sure if the way is correct. I changed var arrayWithQuantity = [Int]() and var arrayWithTime = [Double]() by adding #Published in class getDataFromDatabase (now it's GetDataFromDatabaseViewModel):
class GetDataFromDatabaseViewModel : ObservableObject {
#Published var arrayWithQuantity = [Int]()
#Published var arrayWithTime = [Double]()
func readData() {
let db = Firestore.firestore()
db.collection("amounts").getDocuments { (querySnapshot, err) in
if let e = err{
print("There's any errors: \(e)")
}
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in querySnapshot!.documents{
let quantityFromDb = i.get("amount") as! Int
let timeFromDb = i.get("averageTimeRecognition") as! Double
self.arrayWithQuantity.append(quantityFromDb)
self.arrayWithTime.append(timeFromDb)
}
print("Array with quantity: \(self.arrayWithQuantity.count)")
}
}
}
also in struct I initialized #ObservedObject var gd = GetDataFromDatabaseViewModel() and onAppear now looks like this:
.onAppear{
self.gd.readData()
print("Quantity after reading: \(self.gd.arrayWithQuantity.count)")
}
but print in onAppear still print an empty Array. Where did I do a mistake?

So the problem is in your completion handler. It returns before you retrieve the data.
Solution is to make your arrays #Published and read the data in real time from the view. You have to remove the completion handler.
Call the function on ‚onAppear()‘ and use #ObservedObject to bind to your ViewModel (getDataFromDatabase). This is how it’s done in SwiftUI.
Please capitalize the first letter and use something more generic like „YouViewName“ViewModel.
Your name is fine for a method/function but not for a Class

Related

Saving a list using Codable or userDefaults

Can someone help me to save the list in this code using Codable or another methods. I am not able to use the UserDefaults in the code. Can anyone help me how to use save the lists so that when ever, I re-open my app, the list is still there. Thanks.
import SwiftUI
struct MainView: View {
#State var br = Double()
#State var loadpay = Double()
#State var gp : Double = 0
#State var count: Int = 1
#State var listcheck = Bool()
#StateObject var taskStore = TaskStore()
#State var name = String()
var userCasual = UserDefaults.standard.value(forKey: "userCasual") as? String ?? ""
func addNewToDo() {
taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), toDoItem: "load \(count)", amount: Double(gp)))
}
func stepcount() {
count += 1
}
var body: some View {
VStack {
TextField("Name", text: $name)
HStack {
Button(action: { gp += loadpay }) {
Text("Add Load")
}
Button(action: {
addNewToDo()
}) {
Text("Check")
}
}
Form {
ForEach(self.taskStore.tasks) {
task in
Text(task.toDoItem)
}
}
}
Button(action: {
UserDefaults.standard.set(name, forKey: "userCasual")})
{Text("Save")}
}
}
struct Task : Identifiable {
var id = String()
var toDoItem = String()
var amount : Double = 0
}
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
}
In Task adopt Codable
struct Task : Codable, Identifiable {
var id = ""
var toDoItem = ""
var amount = 0.0
}
In TaskStore add two methods to load and save the tasks and an init method
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
init() {
load()
}
func load() {
guard let data = UserDefaults.standard.data(forKey: "tasks"),
let savedTasks = try? JSONDecoder().decode([Task].self, from: data) else { tasks = []; return }
tasks = savedTasks
}
func save() {
do {
let data = try JSONEncoder().encode(tasks)
UserDefaults.standard.set(data, forKey: "tasks")
} catch {
print(error)
}
}
}
In the view call taskStore.save() to save the data.
However: For large data sets UserDefaults is the wrong place. Save the data in the Documents folder or use Core Data.
Side note: Never use value(forKey:) in UserDefaults, in your example there is string(forKey:)
You should take a look at the #AppStorage property wrapper. Here is a great article written by Paul Hudson who is a great resource when you're learning iOS.
UserDefaults isn't the best way to store persistent information though. Once you get a bit more comfortable with Swift and SwiftUI, you should look into CoreData for storing your data across sessions.

Add an Array element programminglly an pop up "Accessing State's value outside of being installed on a View." alert

I am trying to programminglly add a UI_Element struct to UI_Group array triggered by a socketio listening event, but there is an alert:
"Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not
update."
It seems that I shouldn't access a state's value(item.$name) in ContentView struct, but I don't know how to fix it~QQ
Also, I tried to move the UI_element struct into Service class or ContentView struct, and it couldn't work either.
final class Service: ObservableObject {
private var manager = SocketManager(socketURL: URL(string: "http://x.x.x.x")!, config: [.log(true), .compress])
#Published var messages = [String]()
#Published var UI_Group = [UI_Element]()
init() {
let socket = manager.defaultSocket
socket.on(clientEvent: .connect) {(data, ack) in
print("Connected")
socket.emit("Python Server Port", "Hi Python Server!")
}
socket.on("iOS Client Port") {[weak self] (data, ack) in
if let data = data[0] as? [String: String],
let rawMessage = data["msg"] {
DispatchQueue.main.async {
//self?.messages.append(rawMessage)
switch rawMessage {
case "0":
print("新增中")
self?.UI_Group.append(UI_Element())
if self?.UI_Group != nil {
guard let num_element = self?.UI_Group.count else {
return print("無法新增")
}
print("已新增\(num_element)個元素")
}
default:
print("hello")
}
}
}
socket.connect()
}
}
struct ContentView: View {
#ObservedObject var service = Service()
var body: some View {
VStack{
ForEach(service.UI_Group, id: \.id){ item in
TextField("Please enter text.", text: item.$name)
.padding()
.accessibilityLabel("請輸入元素名稱")
.textFieldStyle(.roundedBorder)
.offset(x: CGFloat(item.center_x), y: CGFloat(item.center_y))
}
}
}
}
struct UI_Element: Identifiable {
let id = UUID()
let type:Int
#State var name = ""
#State var center_x = 0
#State var center_y = 0
#State var ui_state = 0
init() {
self.type = 0
self.name = "請輸入元素名稱"
self.center_x = 50
self.center_y = 50
self.ui_state = 0
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Your assumption:
It seems that I shouldn't access a state's value(item.$name) in ContentView struct
is wrong. The message is because you are using #State in your model 'UI_Element'
Remove the #State wrappers. As the message says they are useless if they are not in a view.
Possible solution:
If you want to use your model in the ContentView with textfield you would need to make your struct 'UI_Element' a class and ObservableObject. Then use #Published on your name property.

Firestore method in view model not invoking in SwiftUI onAppear method

I want to implement a Text field that displays the current user's existing score in the DB (Firestore). Because of the nature of async in Firebase query, I also need to do some adjustment in my codes. However, it seems that completion() handler does not work well:
// ViewModel.swift
import Foundation
import Firebase
import FirebaseFirestore
class UserViewModel: ObservableObject {
let current_user_id = Auth.auth().currentUser!.uid
private var db = Firestore.firestore()
#Published var xp:Int?
func fetchData(completion: #escaping () -> Void) {
let docRef = db.collection("users").document(current_user_id)
docRef.getDocument { snapshot, error in
print(error ?? "No error.")
self.xp = 0
guard let snapshot = snapshot else {
completion()
return
}
self.xp = (snapshot.data()!["xp"] as! Int)
completion()
}
}
}
// View.swift
import SwiftUI
import CoreData
import Firebase
{
#ObservedObject private var users = UserViewModel()
var body: some View {
VStack {
HStack {
// ...
Text("xp: \(users.xp ?? 0)")
// Text("xp: 1500")
.fontWeight(.bold)
.padding(.horizontal)
.foregroundColor(Color.white)
.background(Color("Black"))
.clipShape(CustomCorner(corners: [.bottomLeft, .bottomRight, .topRight, .topLeft], size: 3))
.padding(.trailing)
}
.padding(.top)
.onAppear() {
self.users.fetchData()
}
// ...
}
}
My result kept showing 0 in Text("xp: \(users.xp ?? 0)"), which represents that the step is yet to be async'ed. So what can I do to resolve it?
I would first check to make sure the data is valid in the Firestore console before debugging further. That said, you can do away with the completion handler if you're using observable objects and you should unwrap the data safely. Errors can always happen over network calls so always safely unwrap anything that comes across them. Also, make use of the idiomatic get() method in the Firestore API, it makes code easier to read.
That also said, the problem is your call to fetch data manually in the horizontal stack's onAppear method. This pattern can produce unsavory results in SwiftUI, so simply remove the call to manually fetch data in the view and perform it automatically in the view model's initializer.
class UserViewModel: ObservableObject {
#Published var xp: Int?
init() {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let docRef = Firestore.firestore().collection("users").document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let xp = doc.get("xp") as? Int {
self.xp = xp
} else if let error = error {
print(error)
}
}
}
}
struct ContentView: View {
#ObservedObject var users = UserViewModel()
var body: some View {
VStack {
HStack {
Text("xp: \(users.xp ?? 0)")
}
}
}
}
SwiftUI View - viewDidLoad()? is the problem you ultimately want to solve.

Swift View not updating when Observed Object changes

I have some code like this:
class Data: ObservableObject {
#Published var data = dbContent
init(){
let db = Firestore.firestore()
db.collection("collection").document(userID).addSnapshotListener {
//getting data from DB and storing them as objects by appending them to data
}
}
}
struct 1View: View {
#ObservedObject var myData: Data = Data()
var body: some View {
2View(myData: self.myData)
3View(myData: self.myData)
}
}
struct 2View: View {
#State var myData: Data
var body: some View {
List(){
ForEach(data.count){ data in
Text(data)
}.onDelete(perform: deleteData) //Deletes the item
}
}
}
struct 3View: View {
#State var myData: Data
var body: some View {
List(){
ForEach(data.count){ data in
Text(data)
}.onDelete(perform: deleteData) //Deletes the item
}
}
}
Now the issue is, that I can delete the the item in the 2View. This is then also shown and I implemented the functionality that it deletes the Item in the DB as well.
So the DB data gets altered but this is not shown in the 3View until I refresh it by e.g. revisiting it.
I have no idea what the cause is. Maybe I got a wrong understanding of #Published and ObservedObject ?
#State means that the view owns the data and manages the state. Try using #ObservedObject in your child views as well. Here is an example:
Model
struct Book: Codable, Identifiable {
#DocumentID var id: String?
var title: String
var author: String
var numberOfPages: Int
enum CodingKeys: String, CodingKey {
case id
case title
case author
case numberOfPages = "pages"
}
}
ViewModel
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
private var cancellables = Set<AnyCancellable>()
init() {
fetchData()
}
deinit {
unregister()
}
func unregister() {
if listenerRegistration != nil {
listenerRegistration?.remove()
}
}
func fetchData() {
unregister()
listenerRegistration = db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
func deleteBooks(at offsets: IndexSet) {
self.books.remove(atOffsets: offsets)
}
}
Views
import SwiftUI
struct SampleView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
VStack {
InnerListView1(viewModel: viewModel)
InnerListView2(viewModel: viewModel)
}
}
}
struct InnerListView1: View {
#ObservedObject var viewModel: BooksViewModel
var body: some View {
List {
ForEach(viewModel.books) { book in
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.onDelete { indexSet in
self.viewModel.deleteBooks(at: indexSet)
}
}
}
}
struct InnerListView2: View {
#ObservedObject var viewModel: BooksViewModel
var body: some View {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
}
}
One thing I noticed when trying to reproduce your issue: if you're using CodingKeys (which you only need to do if your the attribute names on the Firestore documents are different from the attribute names on your Swift structs), you need to make sure that the id is also included. Otherwise, id will be nil, which will result in the List view not being abel to tell the items apart.

Convert a #State into a Publisher

I want to use a #State variable both for the UI and for computing a value.
For example, let's say I have a TextField bound to #State var userInputURL: String = "https://". How would I take that userInputURL and connect it to a publisher so I can map it into a URL.
Pseudo code:
$userInputURL.publisher()
.compactMap({ URL(string: $0) })
.flatMap({ URLSession(configuration: .ephemeral).dataTaskPublisher(for: $0).assertNoFailure() })
.eraseToAnyPublisher()
You can't convert #state to publisher, but you can use ObservableObject instead.
import SwiftUI
final class SearchStore: ObservableObject {
#Published var query: String = ""
func fetch() {
$query
.map { URL(string: $0) }
.flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
.sink { print($0) }
}
}
struct ContentView: View {
#StateObject var store = SearchStore()
var body: some View {
VStack {
TextField("type something...", text: $store.query)
Button("search") {
self.store.fetch()
}
}
}
}
You can also use onChange(of:) to respond to #State changes.
struct MyView: View {
#State var userInputURL: String = "https://"
var body: some View {
VStack {
TextField("search here", text: $userInputURL)
}
.onChange(of: userInputURL) { _ in
self.fetch()
}
}
func fetch() {
print("changed", userInputURL)
// ...
}
}
Output:
changed https://t
changed https://ts
changed https://tsr
changed https://tsrs
changed https://tsrst
The latest beta has changed how variables are published so I don't think that you even want to try. Making ObservableObject classes is pretty easy but you then want to add a publisher for your own use:
class ObservableString: Combine.ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<String, Never>()
var string: String {
willSet { objectWillChange.send() }
didSet { publisher.send(string) }
}
init(_ string: String = "") { self.string = string }
}
Instead of #State variables you use #ObservableObject and remember to access the property string directly rather than use the magic that #State uses.
After iOS 14.0, you can access to Publisher.
struct MyView: View {
#State var text: String?
var body: some View {
Text(text ?? "")
.onReceive($text.wrappedValue.publisher) { _ in
let publisher1: Optional<String>.Publisher = $text.wrappedValue.publisher
// ... or
let publisher2: Optional<String>.Publisher = _text.wrappedValue.publisher
}
}
}