SwiftUI: Updating Firestore from DetailView - firebase

I am working on incorporating a Firestore repository into a SwiftUI 2 app. The list view loads appropriately, and refreshes automatically when the data in the Firestore collection is changed. A NavigationLink loads the appropriate DetailView correctly. A button on the DetailView is set to change data within the relevant document, and works correctly, but the DetailView page does not refresh with the correct data (it appears correctly when exiting to the list view and returning); likewise the DetailView will not respond to changes to the collection made elsewhere.
I have attempted to use various configurations of ObservedObject and State, but have been unable to get the result intended.
Any help would be appreciated. Snipped Code listed below shows the flow, and uses the status field as an example.
CustomerRepository
class CustomerRepository: ObservableObject {
private let path: String = "customers"
private let store = Firestore.firestore()
#Published var customers: [Customer] = []
private var cancellables: Set<AnyCancellable> = []
init() {
store.collection(path)
.addSnapshotListener { querySnapshot, error in
if let error = error {
print("Error getting customers \(error.localizedDescription)")
return
}
self.customers = querySnapshot?.documents.compactMap { document in
try? document.data(as: Customer.self)
} ?? []
}
}
func update(_ customer: Customer) {
guard let customerID = customer.id else { return }
do {
try store.collection(path).document(customerID).setData(from: customer)
} catch {
fatalError("Unable to update record: \(error.localizedDescription)")
}
}
}
CustomerListViewModel
class CustomerListViewModel: ObservableObject {
#Published var customerViewModels: [CustomerViewModel] = []
private var cancellables: Set<AnyCancellable> = []
#Published var customerRepository = CustomerRepository()
init() {
customerRepository.$customers.map { customers in
customers.map(CustomerViewModel.init)
}
.assign(to: \.customerViewModels, on: self)
.store(in: &cancellables)
}
}
CustomerViewModel
class CustomerViewModel: ObservableObject, Identifiable {
private let customerRepository = CustomerRepository()
#Published var customer: Customer
private var cancellables: Set<AnyCancellable> = []
var id = ""
init(customer: Customer) {
self.customer = customer
$customer
.compactMap { $0.id }
.assign(to: \.id, on: self)
.store(in: &cancellables)
}
func update(customer: Customer) {
customerRepository.update(customer)
}
}
CustomerListView
struct CustomerListView: View {
#ObservedObject var customerListViewModel = CustomerListViewModel()
var body: some View {
NavigationView {
List {
ForEach(customerListViewModel.customerViewModels) { customerViewModel in
NavigationLink(
destination: CustomerDetailView(customerViewModel: customerViewModel)) {
CustomerCell(customerViewModel: customerViewModel)
}
}
}
}
}
}
CustomerDetailView
struct CustomerDetailView: View {
var customerViewModel: CustomerViewModel
var body: some View {
VStack {
Text(customerViewModel.customer.status.description)
}.onTapGesture {
nextTask()
}
}
private func nextTask() {
switch customerViewModel.customer.status {
case .dispatched:
customerViewModel.customer.status = .accepted
...
default:
return
}
update(customer: customerViewModel.customer)
}
func update(customer: Customer) {
customerViewModel.update(customer: customer)
}
}

After some reconfiguring, and the help of this example:
https://peterfriese.dev/swiftui-firebase-update-data/
I was able to solve my issue. Below is a breakdown of the revised code in case it will help others....
CustomerViewModel
class CustomerViewModel: ObservableObject, Identifiable {
#Published var customer: Customer
#Published var modified = false
private var cancellables: Set<AnyCancellable> = []
init(customer: Customer = Customer(status: .new)) {
self.customer = customer
self.$customer
.dropFirst()
.sing { [weak self] customer in
self?.modified = true
}
.store(in: &cancellables)
}
func handleDoneTapped() {
self.updateOrAddCustomer()
}
}
CustomerListView
// Snip to Navigation Link
NavigationLink(
destination: CustomerDetailView(customerViewModel: CustomerViewModel(customer: customerViewModel.customer))) {
CustomerCell(customerViewModel: customerViewModel)
}
CustomerDetailView
struct CustomerDetailView: View {
#ObservedObject var customerViewModel = CustomerViewModel()
var body: some View {
VStack {
Text(customerViewModel.customer.status.description)
}.onTapGesture {
nextTask()
}
}
private func nextTask() {
switch customerViewModel.customer.status {
case .dispatched:
customerViewModel.customer.status = .accepted
...
default:
return
}
update(customer: customerViewModel.customer)
}
func update(customer: Customer) {
customerViewModel.handleDoneTapped()
}
}

Here you need to put the parameters for which values being updated and want the UI to update accordingly:
class CustomerViewModel: ObservableObject, Identifiable {
private let customerRepository = CustomerRepository()
#Published var customer: Customer
private var cancellables: Set<AnyCancellable> = []
var id = ""
var status: StatusEnum
init(customer: Customer) {
self.customer = customer
self.status = customer.status
$customer
.compactMap { $0.status }
.assign(to: \.status, on: self)
.store(in: &cancellables)
$customer
.compactMap { $0.id }
.assign(to: \.id, on: self)
.store(in: &cancellables)
}
func update(customer: Customer) {
customerRepository.update(customer)
}
}
Then, use this new status variable in the view: Text(customerViewModel.status.description).

Related

Firestore) how to update recyclerview after update data field

I have recyclerview and there is a one button.
This button can change Boolean state from true to false, false to true.
And I want to change the button's background color according to its boolean state.
Below is my code
class ProjectFeedBigAdapter : ListAdapter<Project, ProjectFeedBigAdapter.ProjectFeedBigViewHolder>(
ProjectFeedDiffUtil()
) {
val db: FirebaseFirestore = FirebaseFirestore.getInstance()
val projects: ArrayList<Project> = arrayListOf()
init {
db.collection("projects")
.orderBy("timeStamp", Query.Direction.DESCENDING)
.addSnapshotListener { querySnapshot, exception ->
projects.clear()
if(querySnapshot == null) return#addSnapshotListener
for(snapshot in querySnapshot.documents) {
val project = snapshot.toObject(Project::class.java)
projects.add(project!!)
}
submitList(projects)
}
}
override fun onCreateViewHolder...{...}
override fun onBindViewHolder(holder: ProjectFeedBigViewHolder, position: Int) {
val item = getItem(position)
holder.apply {
when(item.booleanState) {
true -> {
// Button BackgroundColor Change
}
else -> {
// Button BackgroundColor Change
}
}
stateButton.setOnClickListener {
val project = db.collection("projects").document(item.projectId)
item.booleanState?.let {
project
.update("booleanState", !it)
.addOnSuccessListener { Toast.makeText(//context, "Success", Toast.LENGTH_SHORT).show() }
.addOnFailureListener { e -> }
}
}
}
}
inner class ProjectFeedBigViewHolder(private val view: View): RecyclerView.ViewHolder(view) {
...
}
}
class ProjectFeedDiffUtil: DiffUtil.ItemCallback<Project>() {
override fun areItemsTheSame(oldItem: Project, newItem: Project): Boolean {
return oldItem.timeStamp == newItem.timeStamp
}
override fun areContentsTheSame(oldItem: Project, newItem: Project): Boolean {
return oldItem == newItem
}
}
when I click 'stateButton',
Boolean state changes occur. (I can see the data is changing on Firebase Cloud)
Also, can see that pass the submitList().
But not enter onBindViewHolder, so stateButton's background color doesn't change.
How can I solve this problem?

Kotlin searchView does not reload Firebase array

I have one activity with 2 recyclerViews, both recyclers are used with Firebase. One of the recyclers displays the results of a query, the other recycler has a listener that updates every time there is an update in Firebase. I added a searchView to filter the results from Firebase. The issue I'm having is when I'm trying to search the results from the Firebase query, when I start typing I see results but when I click the X to stop searching, the adapter does not reload the array and i don't see the list of items unless I reload the activity. I'm not sure what I'm missing here. Any help/suggestion is greatly appreciated. Here is my code:
Adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Filter
import android.widget.Filterable
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.eduardoz.ezmdapp.Model.Charges
import com.eduardoz.ezmdapp.R
class ChargesAdapter (private var charges: ArrayList<Charges>
, private var chargesAll: ArrayList<Charges>
, private val itemClick: (Charges) -> Unit)
: RecyclerView.Adapter<ChargesAdapter.ViewHolder>()
, Filterable {
inner class ViewHolder(itemView: View, val itemClick: (Charges) -> Unit) :
RecyclerView.ViewHolder(itemView) {
private val chargeCode = itemView.findViewById<TextView>(R.id.chargeCodeTxt)
private val chargeDescription = itemView.findViewById<TextView>(R.id.chargeDescriptionTxt)
fun bindCharges(charges: Charges) {
chargeCode?.text = charges.chargeCode
chargeDescription?.text = charges.chargeDescription
itemView.setOnClickListener { itemClick(charges) }
}
}
init {
this.charges = charges
chargesAll = java.util.ArrayList(charges)
}
override fun getItemCount(): Int {
return charges.count()
}
override fun onBindViewHolder(holder: ChargesAdapter.ViewHolder, position: Int) {
holder.bindCharges(charges[position])
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChargesAdapter.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.charges_list, parent, false)
return ViewHolder(view, itemClick)
}
override fun getFilter(): Filter {
return searchFilter
}
private val searchFilter: Filter = object: Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filteredList: ArrayList<Charges> = ArrayList()
if (constraint!!.isEmpty()) {
filteredList.addAll(chargesAll)
} else {
for(item in chargesAll) {
if
(item.chargeDescription.toLowerCase().contains(constraint.toString().toLowerCase())) {
filteredList.add(item)
}
}
}
val searchResults = FilterResults()
searchResults.values = filteredList
return searchResults
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
charges.clear()
charges.addAll(results!!.values as Collection<Charges>)
notifyDataSetChanged()
}
}
}
Activity
descriptionSearch.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(newText: String): Boolean {
if (newText.isNotEmpty()) {
searchViewBar(newText)
} else {
if (newText.isEmpty()) { //I ADDED THIS TO RELOAD THE ADAPTER
charges.clear()
chargeList()
}
}
return false
}
})
private fun searchViewBar(newText: String) {
chargesListener = chargesCollectionRef
.whereGreaterThanOrEqualTo(CHARGE_DESCRIPTION, newText)
.whereLessThanOrEqualTo(CHARGE_DESCRIPTION, newText+"z")
.addSnapshotListener(this) { snapshot, exception ->
if (exception != null) {
println("error")
}
if (snapshot != null) {
charges.clear()
parseData(snapshot)
}
}
}
fun parseData(snapshot: QuerySnapshot) {
for (document in snapshot.documents) {
val data = document.data
val chargeCode = data!![CHARGE_CODE] as String
val chargeDescription = data[CHARGE_DESCRIPTION] as String
val chargeSpecialty = data[CHARGE_SPECIALTY] as String
val newChargeList = Charges(chargeCode, chargeDescription, chargeSpecialty)
charges.add(newChargeList)
}
chargesFromAdapter.notifyDataSetChanged()
}

Alert.Button is not convertible to Alert.Button?

i try to insert a simple alert to a button, when my Textfields are empty, but I receive the Error message Alert.Button is not convertible to 'Alert.Button?'...
I would say that my code is 100% correct, but.. Xcode is not on my side..
This is my code,..
struct ContentView : View {
#State private var name = String()
#State private var pw = String()
#State private var myAlert = false
var body: some View {
NavigationView {
ZStack{
VStack{
//deleted code//
Button(action: {
if self.name == self.fileName
{
print("Hey \(self.name) - your password is \(self.pw)")
} else {
if self.name.isEmpty && self.pw.isEmpty {
self.myAlert = true
print("broken!")
}
}
.alert(isPresented: $myAlert) {
Alert(title: Text("ERROR!"), message: Text("Username und Password are empty"), dismissButton:.default(Text("Ok")))
}
})
//deleted code//
}}}}}
Does anybody know where the mistake is? I can't found them.
Thanks for your help!
1) There is no fileName?
2) The button is missing a Label:
Button(action: {...}) {
//Label here
}

Passing data from one table to the other on PARSE

everyone been trying to get data from one table using pointers, example User table and songs table, when you tap on a user it displays list of songs in the songs table specific to that users object id i tried using this method but i get an error query.wherekey("" equalTo: (PFUser.current()?.objectId!)!)
HERE IS MY CODE
var users = [""]
var userIDs = [""]
var isFollowing = ["" : true]
override func viewDidLoad() {
super.viewDidLoad()
//load user query
let query = PFUser.query ()
query?.findObjectsInBackground(block: { (objects, error) in
if error != nil {
print(error!)
} else if let users = objects {
for objects in users {
if let user = objects as? PFUser {
self.users.append(user.username!)
self.userIDs.append(user.objectId!)
let query = PFQuery(className: "Followers")
query.whereKey("Follower", equalTo: (PFUser.current()?.objectId!)!)
query.whereKey("Following", equalTo: user.objectId!)
query.findObjectsInBackground(block: { (objects, error) in
if let objects = objects {
if objects.count > 0 {
self.isFollowing[user.objectId!] = true
} else {
self.isFollowing[user.objectId!] = false
}
if self.isFollowing.count == self.users.count {
self.tableView.reloadData()
}
}
})
}
}
}
})
Thank You All
**HERE IS MY CODE FOR THE FIRST VIEW WHERE I AM ABLE TO GET ALL USERS AND SEGUE TO ANOTHER TABLE VIEW CONTROLLER**
//
// users.swift
//
//
// Created by Nawir on 1/17/17.
// Copyright © 2017 Nawir. All rights reserved.
//
import Foundation
import Parse
class artistsviewcontroller: UITableViewController {
var users = [""]
var userIDs = [""]
override func viewDidLoad() {
super.viewDidLoad()
//updating users array
let query = PFUser.query ()
query?.findObjectsInBackground(block: { (objects, error) in
if error != nil {
print(error!)
} else if let users = objects {
for objects in users {
if let user = objects as? PFUser {
self.users.append(user.username!)
self.userIDs.append(user.objectId!)
}
}
}
self.tableView.reloadData()
})
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let Usercell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! mTableViewCell
Usercell.textLabel!.text = users[indexPath.row]
return Usercell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// print("rowtapped: \(indexPath.row)")
let ItemlistTableViewController = self.storyboard?.instantiateViewController(withIdentifier: "itemviewscont") as! itemviewscont
self.navigationController?.pushViewController(ItemlistTableViewController, animated: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
HERE IS MY SECOND TABLEVIEWCONTROLLER
// itemviews.swift
//
//
// Created by Nawir on 1/17/17.
// Copyright © 2017 Nawir. All rights reserved.
//
import Foundation
import Parse
class itemviewscont: UITableViewController {
var users = [String: String] ()
var itemname = [String] ()
override func viewDidLoad() {
super.viewDidLoad()
//query to get all user data from parse
let query = PFUser.query()
query?.findObjectsInBackground { (objects, error) in
if let users = objects {
self.users.removeAll()
for object in users {
if let user = object as? PFUser {
// self.users = [user.objectId!]
self.users[user.objectId!] = user.username!
}
}
}
//query to make connection
let getfollow = PFQuery(className: "Mkitems")
getfollow.whereKey("user", equalTo: PFUser.current()!)
getfollow.findObjectsInBackground(block: { (objects, error) in
if let followers = objects {
for object in followers {
if let follower = object as? PFObject {
let mainuser = follower["User"]
let query = PFQuery(className: "Music")
query.whereKey("fuser", equalTo: mainuser!)
query.findObjectsInBackground(block: { (objects,error) in
if let nitems = objects {
for object in nitems {
if let nitem = object as? PFObject {
self.musicname.append(nitem["name_title"] as! String)
self.tableView.reloadData()
}
}
}
})
}
}
}
})
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemname.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "mscell", for: indexPath)
cell.textLabel?.text = itemname[indexPath.row]
return cell
}
//dont pass this line its the end!
}
THANK YOU ALL
If you searching for current user object (assuming Follower is a pointer ) then use
query.whereKey("Follower", equalTo: PFUser.current()!)
and not :
query.whereKey("Follower", equalTo: (PFUser.current()?.objectId!)!)

Nativescript custom control - Update Dependency Property

I'm creating a custom component which is a list of Buttons.
When the user clicks on a button, i change its css class and then i would like to add it in a custom "selectedItems" property to retrieve it in my ViewModel.
When I do a push on the "selectedItems" array property, no event is raised and I don't get the information.
Also, I tried to re-set the entire array but not better.
I don't know how to achieve this.
Here is the code of my component :
import {WrapLayout} from "ui/layouts/wrap-layout";
import {EventData} from "data/observable";
import {ValueButton} from "./value-button";
import dependencyObservableModule = require("ui/core/dependency-observable");
export class ValuesSelector extends WrapLayout {
public static itemsProperty = new dependencyObservableModule.Property(
"items",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
[],
dependencyObservableModule.PropertyMetadataSettings.None,
function(data: dependencyObservableModule.PropertyChangeData) {
if (data.newValue) {
let instance = <ValuesSelector>data.object;
instance.items = data.newValue;
}
}));
public static deleteOnClickProperty = new dependencyObservableModule.Property(
"deleteOnClick",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
false,
dependencyObservableModule.PropertyMetadataSettings.None));
public static selectedItemsProperty = new dependencyObservableModule.Property(
"selectedItems",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
[],
dependencyObservableModule.PropertyMetadataSettings.None));
public static singleSelectionProperty = new dependencyObservableModule.Property(
"singleSelection",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
false,
dependencyObservableModule.PropertyMetadataSettings.None));
public get selectedItems() {
return this._getValue(ValuesSelector.selectedItemsProperty);
}
public set selectedItems(value: any[]) {
this._setValue(ValuesSelector.selectedItemsProperty, value);
}
public get deleteOnClick() {
return this._getValue(ValuesSelector.deleteOnClickProperty);
}
public set deleteOnClick(value: boolean) {
this._setValue(ValuesSelector.deleteOnClickProperty, value);
}
public get singleSelection() {
return this._getValue(ValuesSelector.singleSelectionProperty);
}
public set singleSelection(value: boolean) {
this._setValue(ValuesSelector.singleSelectionProperty, value);
}
public get items() {
return this._getValue(ValuesSelector.itemsProperty);
}
public set items(value: any) {
this._setValue(ValuesSelector.itemsProperty, value);
this.createUI();
}
private _buttons: ValueButton[];
constructor() {
super();
this.orientation = "horizontal";
this._buttons = [];
}
private createUI() {
this.removeChildren();
let itemsLength = this.items.length;
for (let i = 0; i < itemsLength; i++) {
let itemButton = new ValueButton();
itemButton.text = this.items[i].label;
itemButton.value = this.items[i];
itemButton.className = "values-selector-item";
if (this.deleteOnClick) {
itemButton.className = "values-selector-selected-item";
}
itemButton.on(ValueButton.tapEvent, (data: EventData) => {
let clickedButton = <ValueButton>data.object;
if (this.deleteOnClick) {
let itemIndex = this.items.indexOf(clickedButton.value);
if (itemIndex > -1) {
let newSelectedItems = this.items;
newSelectedItems.splice(itemIndex, 1);
this.items = newSelectedItems;
}
return;
}
let internalSelectedItems = this.selectedItems;
if (clickedButton.className === "values-selector-item") {
if (this.singleSelection && this.selectedItems.length > 0) {
internalSelectedItems = [];
for (let i = 0; i < this._buttons.length; i++) {
this._buttons[i].className = "values-selector-item";
}
}
internalSelectedItems.push(clickedButton.value);
clickedButton.className = "values-selector-selected-item";
} else {
let itemIndex = internalSelectedItems.indexOf(clickedButton.value);
if (itemIndex > -1) {
internalSelectedItems.splice(itemIndex, 1);
}
clickedButton.className = "values-selector-item";
}
this.selectedItems = internalSelectedItems;
}, this);
this._buttons.push(itemButton);
this.addChild(itemButton);
}
}
}
Can you help me ?
Thanks
Ok I made a mistake by databinding my property.
In fact, in the XML I use the component like this :
<vs:ValuesSelector items="{{ criterias }}" selectedItems="{{ myObject.selectedCriterias }}" />
But in the ViewModel, I never initialized the selectedCriterias property because I thought that the default value [] specified in the component would create it.
So in the ViewModel, here is the fix :
Before
this.myObject = {
id : 0
};
After
this.myObject = {
id : 0,
selectedCriterias: []
};

Resources