How to use Realm with SwiftUI forms - realm

Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.'
All changes to a managed object (addition, modification and deletion) must be done within a write transaction. For example,
// Update an object with a transaction
try! realm.write {
author.name = "Thomas Pynchon"
}
I can make a Realm sub-class conform to ObservableObject. However, I don't see how to make the realm properties updatable in SwiftUI. Realm property example below.
#objc dynamic var myName: String = "Adam"
Realm automagically sets up the schema based on #objc dynamic var. I don't see a way to get #Published on a realm property. SwiftUI will render a TextField, but crashes when the value is edited.
TextField("Quantity (\(shoppingItem.myUnit!.myName))", value: $shoppingItem.stdQty, formatter: basicFormat)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numbersAndPunctuation)
Is there any way to wrap SwiftUI state changes inside a Realm write transaction?

Another way to do this is to use a Custom Binding, and when setting the property, open a transaction and save data to realm.
TextField("Quantity (\(shoppingItem.myUnit!.myName))",
value: Binding(get: {shoppingItem.stdQty},
set: {(value) in
//if your state property is in a view model, all this code could be like viewModel.saveData(newMessage: value)
let realm = try! Realm()
try! realm.write {
shoppingItem.stdQty = value
}
}),
formatter: basicFormat)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numbersAndPunctuation)
This will save to realm on every character inserted

Consider the Realm property, stdQty shown below. It can only be changed within a write transaction.
import RealmSwift
import Combine
class ShoppingItems: Object, ObservableObject
let objectWillChange = PassthroughSubject<Void, Never>()
#objc dynamic var stdQty: Double = 1
You cannot bind stdQty without the error in the original question. However you can create a calculated variable that can be bound.
var formQty: Double {
get {
return stdQty
}
set {
objectWillChange.send()
if let sr = self.realm {
try! sr.write {
stdQty = newValue
}
}
}
}
Binding the calculated variable works fine.
TextField("Quantity (\(shoppingItem.myUnit!.myName))", value: $shoppingItem.formQty, formatter: basicFormat)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numbersAndPunctuation)
Answer limitation: objectWillChange is only triggered by the calculated variable. Changes in the form are reflected in the form. Changes in the realm don't trigger a Combine objectWillChange yet.

Related

Swift 3, Firebase completion Handler is executed twice

My Use case -
I have list of items which I fetch from Firebase. Below is loadItems() function that I call from HomeViewController -
viewDidLoad() and updating tableView with the fetched data.
func loadItems() {
Database.database().reference().child("items").observe(.value, with: { snapshot in
var fetchedItems = [Item]()
guard let receivedvalue = snapshot.value as? [String: Any] else {
print("Received null")
return
}
print(receivedvalue)
for (key, value) in receivedvalue {
let item = Item(id: Int(key)!, json: value as! [String : Any])
fetchedItems.append(item!)
}
self.items = fetchedItems
self.tableView.reloadData()
})
}
I am saving an item and coming back from CreateViewController to HomeViewController, I am - Saving the item in Firebase, Appending the item to prefetched array, reloading tableView.
func addItem(item: Item?) {
rootRef = Database.database().reference()
let id = String(describing: item.id!)
let itemRef = self.rootRef.child("items").child(id)
itemRef.setValue(["name": item.name!, "type": item.type!])
items.append(item!)
self.tableView.reloadData()
}
After reloading tableView, its is going in the Firebase GET Call handler which is present in loadItems().
The handler is executed once when I am getting all items during viewDidLoad(). Is there any reason why the Firebase GET call handler is executed the second time even though I am not calling loadItems() in create workflow?
When .observe(.value is used, it adds an observer to that node and any changes to that node (add, change, remove) will fire the code in the closure.
If you want to leave the observer so you can be notified of changes, the the proper flow is to simply write the data to Firebase and let the closure load the data and populate the tableView.
However, the downside to this is that .value loads ALL of the data in the node. You may want to take a look at adding separate observers for .childAdded, .childChanged and .childRemoved. Those will only load the node that was modified.
If you want to only load the data once (to populate a dataSource on startup for example), use observeSingleEvent which fires once and does not leave an observer.
Then store the data in Firebase and manually add it to the array and reload the tableView.
See the documentation Read Data Once section.

When utilizing the .push method can I write a copy of the id to the object?

I'm using the .push method on firebase to write new records. I'd like to save the key where the new record is saved to the record itself at the id key. Currently, I do this in 2 operations, first push the record and then update using the ref returned. Can I do this in 1 write? Does it not matter?
If you invoke the Firebase push() method without arguments it is a pure client-side operation.
var newRef = ref.push(); // this does *not* call the server
You can then add the key() of the new ref to your item:
var newItem = {
name: 'anauleau'
id: newRef.key()
};
And write the item to the new location:
newRef.set(newItem);
There's no method to do this in one operation. However, it typically does not matter, because you can always get the push id from the .key() method on the DataSnapshot.
But, there's nothing wrong either about storing the push id. So you coul create a function on the Firebase prototype.
Firebase.prototype.pushWithId = function pushWithid(data) {
var childRef = this.push();
data.key = childRef.key();
childRef.update(data); // or .set() depending on your case
return childRef;
};
var ref = new Firebase('<my-firebase-app>');
ref.pushWithId({ name: 'Alice' });
Take caution with modifying the prototype of functions you do not own. In this case, you'll likely be fine. This method does little, and there's not much of a chance that the Firebase SDK gains a .pushWithId() method.

$firebaseObject.$save() overwrites the whole node

I have a plunker where I have a simple update-Method to update a recorcd. But when I add or update a property to the record, the whole record will be overwritten.
The Data
{
"org1" : {
"description" : "too lazy",
"name" : "Apple",
"website" : "http://www.apple.com"
},
"org2" : {
"name" : "Google",
"test1" : "test123",
"website" : "http://www.google.com"
}
}
update()
this.update = function() {
//var obj = new Organization('org2');
var obj = $firebaseObject(new Firebase('https://thefactorymess.firebaseio-demo.com/organizations/org2'));
obj.test1 = 'test123';
obj.$save().then(function() {
console.log('org new', obj);
});
};
Here is the plunker.
Can someone explain me this behavior?
The AngularFire $save() method indeed overwrites the current data at that location. It is implemented by calling the set() method of Firebase's JavaScript SDK, which does precisely that.
Since you're calling $save() immediately after creating the reference, the data hasn't been loaded from the Firebase servers yet. So you're creating an object with a single properties test1. When you then $save() that object to the server, you end up with an object with a single property there too.
If you only want to update some properties, you can call update().
var ref = new Firebase('https://thefactorymess.firebaseio-demo.com/organizations/org2');
ref.update({ test1: 'test123' });
This doesn't use AngularFire. But since AngularFire is built on top of the regular Firebase JavaScript SDK, they interoperate without problems.
Alternatively you could use AngularFire's $loaded() promise to wait for the object to have loaded completely. But that would lead to both a lot more code and pulling down data.
My rule of thumb is:
Only use AngularFire for binding things to the $scope. For everything else, use the Firebase JavaScript SDK.

angularFireCollection is not returning any data

I have no issues when using implicit updates (angelFire). However I need for some of my data use explicit updating. So I implemented angelFireCollection on the exact same ref I was using previously but despite the console.log explicitly saying that the read was granted and trying it with both with the onloadcallback and without, I don't get data directly into my assigned variable AND once the callback fires I get a strange looking object that DOES contain the data but not in the form I expect. My scope variable ends up with an empty collection. Never gets populated. Here is the code:
var streamController = function ($rootScope, $scope, $log, $location, angularFireCollection, profileService) {
//Wait for firebaseLogin...
$rootScope.$watch('firebaseAuth', init);
function init() {
if ($rootScope.firebaseAuth == false) {
return
};
var refUsers = new Firebase($rootScope.FBURL+'/users/'+$rootScope.uid);
$scope.profile = angularFireCollection(refUsers, function onload(snapshot) {
console.log(snapshot)
});
};
};
myApp.gwWebApp.controller('StreamController', ['$rootScope', '$scope', '$log', '$location', 'angularFireCollection', 'profileService',
streamController]);
}());
Here is what the console.log looks like ( ie; what snapshot looks like ):
>snapshot
T {z: R, bc: J, V: function, val: function, xd: function…}
Here is the earlier message before the snapshot was returned:
Firebase Login Succeeded! fbLoginController.js:16
FIREBASE: Attempt to read /users/529ccc5d1946a93656320b0a with auth={"username":"xxxxxxx#me.com","id":"529ccc5d1946a93656320b0a"} firebase.js:76
FIREBASE: /: "auth.username == 'admin'" firebase.js:76
FIREBASE: => false firebase.js:76
FIREBASE: /users firebase.js:76
FIREBASE: /users/529ccc5d1946a93656320b0a: "auth.id == $user" firebase.js:76
FIREBASE: => true firebase.js:76
FIREBASE:
FIREBASE: Read was allowed.
and finally the desired binding that ends up with an empty array: again from the console:
$scope.profile
[]
Anyone know what I could possibly be doing wrong?? This is like 5 lines of code. Frustrating.
I have put stops in angelFireCollection factory function and can see that the data is getting added to the collection in the callbacks inside that function but my binded variable never gets updated.
UPDATE
Ok experimenting with a plnkr. It seems that angularFireCollection EXPECTS your returning a LIST of items. The snapshot returns properly if you inspect snapshot.val() it will be whatever object structure was stored in firebase. IF you use angularFireCollection it does indeed bind to the variable HOWEVER it turns a non-list object into a garbled mess and you can not access the object user the normal dot operator. This is either a bug or it is a severe limitation of angularFireCollection which will cause me to revaluate how easily I can use firebase as the backend. I can't share my plnkr because it is accessing non-public data but tomorrow if i have time I will create a public firebase with an object store and demonstrate.
Ok. So it appears that indeed angularFireCollection is MEANT to be array based. Which is fine. It would be VERY helpful if the angularFire documentation was updated to make that clear. As such it is not an implicit vs explicit update technique.
For an explicit non-array based approach I have come up with the following code. Had I not been mislead by the documentation I would have gone down this path originally.
var MainCtrl = function($scope, angularFire) {
$scope.test = {};
var _url = 'https://golfwire.firebaseio.com/tmp';
var _ref = new Firebase(_url);
var promise = angularFire(_ref, $scope, 'implicit');
promise.then ( function(data){
$scope.explicit=angular.copy($scope.implicit );
});
}
You then work locally with the 'explicit' copy and when ready just update the 'implicit' by assigning: $scope.implicit = $scope.explicit.
Here is a plnkr: http://plnkr.co/edit/bLJrL1

how to discard initial data in a Firebase DB

I'm making a simple app that informs a client that other clients clicked a button. I'm storing the clicks in a Firebase (db) using:
db.push({msg:data});
All clients get notified of other user's clicks with an on, such as
db.on('child_added',function(snapshot) {
var msg = snapshot.val().msg;
});
However, when the page first loads I want to discard any existing data on the stack. My strategy is to call db.once() before I define the db.on('child_added',...) in order to get the initial number of children, and then use that to discard that number of calls to db.on('child_added',...).
Unfortunately, though, all of the calls to db.on('child_added',...) are happening before I'm able to get the initial count, so it fails.
How can I effectively and simply discard the initial data?
For larger data sets, Firebase now offers (as of 2.0) some query methods that can make this simpler.
If we add a timestamp field on each record, we can construct a query that only looks at new values. Consider this contrived data:
{
"messages": {
"$messageid": {
"sender": "kato",
"message": "hello world"
"created": 123456 // Firebase.ServerValue.TIMESTAMP
}
}
}
We could find messages only after "now" using something like this:
var ref = new Firebase('https://<your instance>.firebaseio.com/messages');
var queryRef = ref.orderBy('created').startAt(Firebase.ServerValue.TIMESTAMP);
queryRef.on('child_added', function(snap) {
console.log(snap.val());
});
If I understand your question correctly, it sounds like you only want data that has been added since the user visited the page. In Firebase, the behavior you describe is by design, as the data is always changing and there isn't a notion of "old" data vs "new" data.
However, if you only want to display data added after the page has loaded, try ignoring all events prior until the complete set of children has loaded at least once. For example:
var ignoreItems = true;
var ref = new Firebase('https://<your-Firebase>.firebaseio.com');
ref.on('child_added', function(snapshot) {
if (!ignoreItems) {
var msg = snapshot.val().msg;
// do something here
}
});
ref.once('value', function(snapshot) {
ignoreItems = false;
});
The alternative to this approach would be to write your new items with a priority as well, where the priority is Firebase.ServerValue.TIMESTAMP (the current server time), and then use a .startAt(...) query using the current timestamp. However, this is more complex than the approach described above.

Resources