SwiftUI Fetching contacts duplication - fetch

I have a model view that has fetching contacts function:
class ContactsStore: ObservableObject {
#Published var contacts = [CNContact]()
func fetch() {} ...
And then in my View:
#EnvironmentObject var store: ContactsStore
var groupedContacts: [String: [CNContact]] {
.init (
grouping: store.contacts,
by: {$0.nameFirstLetter}
)
}
...
List() {
ForEach(self.groupedContacts.keys.sorted(), id: \.self) { key in ...
I've shortened my code for the convenience, will add/edit if needed. The issue I faced - every time my View is rendered the fetch function is called and my array of contacts is duplicated in my view. TIA for a help
UPD: Duplication happens due to method calling fetch in the List .onAppear. So I'm looking at how to call this method only once and not every time the View appears.

You can do fetch in Init() like that:
struct someView: View{
var list: [Settings]
init(){
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Settings> = Settings.fetchRequest()
//... some order and filter if you need
self.list = context.fetch(fetchRequest)
}
var body: some View{
...
ForEach(list){settings in
...
}
...
}
}
Didn't try it with grouping, but you asked how to fetch only once. The answer is - in init().
But you cant get the #Environment in init, so i get the context from AppDelegate. Or you can pass the context as init parameter

Related

MVVM with Repository/Firestore - Where is the best place to store different queried arrays from a single collection?

I am building a ToDo List App using Firestore based on this google tutorial, with a SwiftUI app using an MVVM/repository pattern, that uses one load query to find all tasks ("actions"), and I'm trying to set it up so I can have multiple date-based queries (e.g. display dates for today, for next week, possibly on the same screen)
The current version I'm working on has one single "loadData" function in the repository that is saved to a single published "actions" variable, and called when this is initialized.
class ActionRepository: ObservableObject, ActionStoreType {
let db = Firestore.firestore()
#Published var actions = [Action]()
init() {
loadData()
}
func loadData() {
let userId = Auth.auth().currentUser?.uid
db.collection("action")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId!)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.actions = querySnapshot.documents.compactMap { document in
do {
let x = try document.data(as: Action.self)
return x
}
catch {
print(error)
}
return nil
}
}
}
}
My view model just calls the repository with no parameters.
class ActionListViewModel: ObservableObject {
#Published var actionRepository = ActionRepository()
#Published var actionCellViewModels = [ActionCellViewModel]()
private var cancellables = Set<AnyCancellable>()
init() {
actionRepository.$actions.map { actions in
actions.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.actionCellViewModels, on: self)
.store(in: &cancellables)
}
I want to add a function to load data by date that I can call as many times as I want:
func loadMyDataByDate2(from startDate: Date, to endDate: Date? = nil) {
let userId = Auth.auth().currentUser?.uid
let initialDate = startDate
var finalDate: Date
if endDate == nil {
finalDate = Calendar.current.date(byAdding: .day, value: 1, to: initialDate)!
} else {
finalDate = endDate!
}
db.collection("action")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId!)
.whereField("startDate", isGreaterThanOrEqualTo: initialDate)
.whereField("startDate", isLessThanOrEqualTo: finalDate)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.actions = querySnapshot.documents.compactMap { document in
do {
let x = try document.data(as: Action.self)
return x
}
catch {
print(error)
}
return nil
}
}
}
But I don't know the best way to do this. If I want my view model to have three lists of tasks: one for today, one for the rest of the week, and one for next week, what would be the best way to do this?
Should I create separate variables in the repository or the View Model to store these different lists of actions?
Or add date variables for the repository so I call multiple instances of it within the View Model?
I just want to make sure I'm not going down an unwise path with how I start building this out.
I ended up doing something based on Peter's suggestion. I am getting all these filtered lists in my ViewModel. Instead of taking from the repository and storing them all in one ActionCellViewModel property, I created four different ActionCellViewModel properties.
I have four different functions in my initializer code now, and each one takes the list of actions, filters it based on date and completion status, and assigns it to the appropriate CellViewModel property for use in my view.
class ActionListViewModel: ObservableObject {
#Published var actionRepository: ActionStoreType
#Published var baseDateActionCellViewModels = [ActionCellViewModel]()
#Published var baseDateWeekActionCellViewModels = [ActionCellViewModel]()
#Published var previousActionCellViewModels = [ActionCellViewModel]()
#Published var futureActionCellViewModels = [ActionCellViewModel]()
#Published var baseDate: Date = Date()
#Published var hideCompleted: Bool = true
#Published var baseDateIsEndOfWeek: Bool = false
private var cancellables = Set<AnyCancellable>()
// MARK: Initializers
// Default initializer for production code.
init() {
self.actionRepository = ActionRepository()
self.baseDateIsEndOfWeek = isDateEndOfWeek(date: self.baseDate, weekEnd: self.baseDate.endOfWeekDate(weekStart: .sat))
loadPastActions()
loadBaseActions()
loadWeekTasks()
loadFutureActions()
}
// MARK: Functions for initializing the main groups of actions for the Homepage.
func isDateEndOfWeek(date currentDate: Date, weekEnd endOfWeekDate: Date) -> Bool {
if currentDate == endOfWeekDate {
print("Current Date: \(currentDate) and endOfWeekDate: \(endOfWeekDate) are the same!")
return true
} else {
print("The current date of \(currentDate) is not the end of the week (\(endOfWeekDate))")
return false
}
}
///The loadPastActions function takes the published actions list from the repository, and pulls a list of actions from before the base date. (It hides completed actions by default, but this is impacted by the viewModel's "hideCompleted" parameter.
///
///- returns: Assigns a list of actions from prior to the base date to the pastActionCellViewModels published property in the viewModel.
func loadPastActions() {
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.beforeDate(self.baseDate) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.previousActionCellViewModels, on: self)
.store(in: &cancellables)
}
///The loadBaseActions function takes the published actions list from the repository, and pulls a list of actions from the base date. (It hides completed actions by default, but this is impacted by the viewModel's "hideCompleted" parameter.
///
///- returns: Assigns a list of actions from the base date to the viewModel's baseDateActionCellViewModels property.
func loadBaseActions() {
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.inDateRange(from: self.baseDate, to: self.baseDate) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.baseDateActionCellViewModels, on: self)
.store(in: &cancellables)
}
/// The loadWeekActions takes the published actions list for the current user from the repository, and pulls a list of actions either from remainder of the current week (if not the end of the week), or from next week, if it's the last day of the week.
///
///- returns: Assigns a list of actions from the rest of this week or the next week to the viewModel's baseDateWeekActionCellViewModels property.
func loadWeekTasks() {
let startDate: Date = self.baseDate.tomorrowDate()
print("Start date is \(startDate) and the end of that week is \(startDate.endOfWeekDate(weekStart: .sat))")
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.inDateRange(from: startDate, to: startDate.endOfWeekDate(weekStart: .sat)) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.baseDateWeekActionCellViewModels, on: self)
.store(in: &cancellables)
}
/// The loadFutureActions function takes the published actions list for the current user from the repository, and pulls a list of actions from after the week tasks.
///
///- returns: Assigns a list of actions from the future (beyond this week or next, depending on whether the baseDate is the end of the week) to the futureActionCellViewModels property in the viewModel.
func loadFutureActions() {
let startAfter: Date = baseDate.tomorrowDate().endOfWeekDate(weekStart: .sat)
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.afterDate(startAfter) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.futureActionCellViewModels, on: self)
.store(in: &cancellables)
}

how to publish the data of a network request using Combine framework and SwiftUI

I'm developing an iOS app using swiftUI and Combine framework and also MVVM.
I'm want to handle the login API request in a separate class called LoginService, which is used in the LoginViewModel.
Now I want to know how should I publish and observe the attributes between view and ViewModel.
I mean ViewModel is an ObservableObject and is being observed in the View, But since I'm handling the network request in a Service class how should LoginService notify LoginViewModel and LoginView that the data is received and the View should be updated?
import Foundation
import Combine
class LoginViewModel: ObservableObject {
#Published var user = UserModel()
#Published var LoginStatus: Bool = false
#Published var LoginMessage: String = ""
var service = LoginService()
func Login(With email: String, And password: String) -> Bool {
service.validateLogin(email: email, password: password)
return false
}
}
This is the Code for LoginViewModel.
How should LoginService change the values for LoginStatus, LoginMessage and user when data is received from the server to notify the View?
I'm saying this because as far as I know you can Observe the ObservableObjects only in the View(SwiftUI).
Okay so I take your example and I would do the following:
I assumed your service returns true or false
import Foundation
import Combine
class LoginViewModel: ObservableObject {
#Published var LoginStatus: Bool = false
#Published var LoginMessage: String = ""
var service = LoginService()
func Login(_ email: String, _ password: String) -> Bool {
self.LoginStatus = service.validateLogin(email: email, password: password)
return self.LoginStatus
}
}
In your view:
import SwiftUI
struct ContentView : View {
#ObservedObject var model = LoginViewModel()
var body: some View {
VStack {
Button(action: {
_ = self.model.Login("TestUser", "TestPassword")
}, label: {
Text("Login")
})
Text(self.model.LoginStatus ? "Logged In" : "Not Logged in")
}
}
}
It should be something around that.
I removed UserModel because you shouldn't nest models.
I hope this helps.
EDIT 1:
To validate something automatically you can use onApear() on your View
Or listen to changes with olnrecieve() to update the UI or the State
import SwiftUI
struct ContentView : View {
#ObservedObject var model = LoginViewModel()
var body: some View {
VStack {
Button(action: {
_ = self.model.Login("TestUser", "TestPassword")
}, label: {
Text("Login")
})
Text(self.model.LoginStatus ? "Logged In" : "Not Logged in")
}.onAppear {
// call a function that gets something from your server
// and modifies your state
self.model.validate()
}.onReceive(self.model.$LoginMessage, perform: { message in
// here you can update your state or your ui
// according the LoginMessage... this gets called
// whenever LoginMessage changes in your model
})
}
}
You've established a one-way binding, i.e; model -> view
Now it doesn't matter how your service updates model, whenever model changes, view updates.
There are several ways for your service to update model.
Usual URLSession, update LoginStatus and LoginMessage in callback as usual.
Via Combine publishers, e.g.; URLSessionDataTaskPublisher, and do update in its sink closure.

Flutter - Dart : wait a forEach ends

I try to modify a string using data in each node I retrieve from Firebase database, and then to write a file with the modifed string (called "content").
Here is what I tried :
// Retrieve initial content from Firebase storage
var data = await FirebaseStorage.instance.ref().child("...").getData(1048576);
var content = new String.fromCharCodes(data);
// Edit content with each node from Firebase database
final response = await FirebaseDatabase.instance.reference().child('...').once();
response.value.forEach((jsonString) async {
...
// cacheManager.getFile returns a Future<File>
cacheManager.getFile(signedurl).then((file){
// Modify content
content=content.replaceAll('test',file.path);
});
});
// Finally write the file with content
print("test");
final localfile = File('index.html');
await localfile.writeAsString(content);
Result :
"test" is shown before the forEach ends.
I found that we can do in Dart (https://groups.google.com/a/dartlang.org/forum/#!topic/misc/GHm2cKUxUDU) :
await Future.forEach
but in my case if I do : await Future.response.value.forEach (sounds a bit weird)
then I get :
Getter not found: 'response'.
await Future.response.value.forEach((jsonString) async {
How to wait that forEach ends (with "content" modified) before to write the file with new content?
Any idea?
If you use for(... in ) instead of forEach you can use async/await
Future someMethod() async {
...
for(final jsonString in response.value) {
...
// cacheManager.getFile returns a Future<File>
cacheManager.getFile(signedurl).then((file){
// Modify content
content=content.replaceAll('test',file.path);
});
});
}
With forEach the calls fire away for each jsonString immediately and inside it await works, but forEach has return type void and that can't be awaited, only a Future can.
You defined the callback for forEach as async, which means that it runs asynchronously. In other words: the code inside of that callback runs independently of the code outside of the callback. That is exactly why print("test"); runs before the code inside of the callback.
The simplest solution is to move all code that needs information from within the callback into the callback. But there might also be a way to await all of the asynchronous callbacks, similar to how you already await the once call above it.
Update I got working what I think you want to do. With this JSON:
{
"data" : {
"key1" : {
"created" : "20181221T072221",
"name" : "I am key1"
},
"key2" : {
"created" : "20181221T072258",
"name" : "I am key 2"
},
"key3" : {
"created" : "20181221T072310",
"name" : "I am key 3"
}
},
"index" : {
"key1" : true,
"key3" : true
}
}
I can read the index, and then join the data with:
final ref = FirebaseDatabase.instance.reference().child("/53886373");
final index = await ref.child("index").once();
List<Future<DataSnapshot>> futures = [];
index.value.entries.forEach((json) async {
print(json);
futures.add(ref.child("data").child(json.key).once());
});
Future.wait(futures).then((List<DataSnapshot> responses) {
responses.forEach((snapshot) {
print(snapshot.value["name"]);
});
});
Have you tried:
File file = await cacheManager.getFile(signedurl);
content = content.replaceAll('test', file.path);
instead of:
cacheManager.getFile(signedurl).then((file){ ...});
EDIT:
Here's a fuller example trying to replicate what you have. I use a for loop instead of the forEach() method:
void main () async {
List<String> str = await getFuture();
print(str);
var foo;
for (var s in str) {
var b = await Future(() => s);
foo = b;
}
print('finish $foo');
}
getFuture() => Future(() => ['1', '2']);

Returning values from a subscription callback

I am trying to get the value of a field from a document returned via a subscription. The subscription is placed inside a helper function. I had a callback function within the subscription return this value and then I assigned the return value to a variable (see code). Finally, I had the helper return this value. However, the value returned is a subscription object (?) and I can't seem to get anything out of it.
Code:
Template.myTemplate.helpers({
'returnUser':function(){
var id = Session.get('currentUserId');
var xyz = Meteor.subscribe('subscriptionName',id,function(){
var user = accounts.find().fetch({_id: id})[0].username;
return user;
}
return xyz;
}
});
Any help will be much appreciated. :)
You have to load first your subscriptions when the template is created, this creates an instance of your data with Minimongo.
Template.myTemplate.onCreated(function () {
var self = this;
self.autorun(function() {
self.subscribe('subscriptionName',id);
});
});
Then in the helper, you can make a query to retrieve your data
Template.myTemplate.helpers({
'returnUser': function(){
var id = Session.get('currentUserId');
return accounts.findOne(id).username;
}
});

Duplicate data on push/add to firebase using angularfire

Is there a way (maybe using rules) to duplicate data on add/push to firebase?
What I want to archive is when I do an add to a firebase array I want to duplicate the data to another array.
So this is my firebase structure:
my-firebase: {
items: [ ... ],
queue: [ ... ]
}
And this is how I have my services defined:
.factory('Service1',['$firebaseArray', function($firebaseArray) {
var items = new Firebase('my-firebase.firebaseio.com/items');
return $firebaseArray(items);
}])
.factory('Service2',['$firebaseArray', function($firebaseArray) {
var queue = new Firebase('my-firebase.firebaseio.com/queue');
return $firebaseArray(queue);
}])
And here is how I use them:
.controller('controller', function($scope, Service1, Service2) {
$scope.save = function() {
Service1.$add({name: "test1"});
Service2.$add({name: "test1"});
}
};
And want I to have a single call not a duplicate call/code but having the result in both arrays (items and queue).
Thanks so much!
Always remember that AngularFire is a relatively thin wrapper around Firebase's JavaScript SDK that helps in binding data into your AngularJS views. If you're not trying to bind and something is not immediately obvious, you'll often find more/better information in the documentation of Firebase's JavaScript SDK.
The API documentation for $firebaseArray.$add() is helpful for this. From there:
var list = $firebaseArray(ref);
list.$add({ foo: "bar" }).then(function(ref) {
var id = ref.key();
console.log("added record with id " + id);
list.$indexFor(id); // returns location in the array
});
So $add() returns a promise that is fulfilled when the item has been added to Firebase. With that knowledge you can add a same-named child to the other list:
var queue = new Firebase('my-firebase.firebaseio.com/queue');
$scope.save = function() {
Service1.$add({name: "test1"}).then(function(ref) {
queue.child(ref.key().set({name: "test1"});
});
}
This last snippet uses a regular Firebase reference. Since AngularFire builds on top of the Firebase JavaScript SDK, they work perfectly together. In fact: unless you're binding these $firebaseArrays to the $scope, you're better off not using AngularFire for them:
var items = new Firebase('my-firebase.firebaseio.com/items');
var queue = new Firebase('my-firebase.firebaseio.com/queue');
$scope.save = function() {
var ref = queue.push();
ref.set({name: "test1"})
queue.child(ref.key().set({name: "test1"});
}
To my eyes this is much easier to read, because we're skipping a layer that wasn't being used. Even if somewhere else in your code, you're binding a $firebaseArray() or $firebaseObject() to the same data, they'll update in real-time there too.
Frank's answer is authoritative. One additional thought here is that AngularFire is extremely extensible.
If you want data pushed to two paths, you could simply override the $add method and apply the update to the second path at the same time:
app.factory('DoubleTap', function($firebaseArray, $q) {
var theOtherPath = new Firebase(...);
return $firebaseArray.$extend({
$add: function(recordOrItem) {
var self = this;
return $firebaseArray.prototype.$add.apply(this, arguments).then(function(ref) {
var rec = self.$getRecord(ref.key());
var otherData = ...do something with record here...;
return $q(function(resolve, reject) {
theOtherPath.push(rec.$id).set(otherData);
});
});
}
});
});

Resources