Automatic UI updates with Apollo in Swift not working - ios

I have the following setup for a small Apollo iOS app where I display a list of conferences in a table view and want to be able to add a conference to the list:
GraphQL:
query AllConferences {
allConferences {
...ConferenceDetails
}
}
mutation CreateConference($name: String!, $city: String!, $year: String!) {
createConference(name: $name, city: $city, year: $year) {
...ConferenceDetails
}
}
fragment ConferenceDetails on Conference {
id
name
city
year
attendees {
...AttendeeDetails
}
}
fragment AttendeeDetails on Attendee {
id
name
conferences {
id
}
}
ConferencesTableViewController:
class ConferencesTableViewController: UITableViewController {
var allConferencesWatcher: GraphQLQueryWatcher<AllConferencesQuery>?
var conferences: [ConferenceDetails] = [] {
didSet {
tableView.reloadData()
}
}
deinit {
allConferencesWatcher?.cancel()
}
override func viewDidLoad() {
super.viewDidLoad()
allConferencesWatcher = apollo.watch(query: AllConferencesQuery()) { result, error in
print("Updating conferences: ", result?.data?.allConferences)
guard let conferences = result?.data?.allConferences else {
return
}
self.conferences = conferences.map { $0.fragments.conferenceDetails }
}
}
// ...
// standard implementation of UITableViewDelegate
// ...
}
AddConferenceViewController:
class AddConferenceViewController: UIViewController {
// ... IBOutlets
#IBAction func saveButtonPressed() {
let name = nameTextField.text!
let city = cityTextField.text!
let year = yearTextField.text!
apollo.perform(mutation: CreateConferenceMutation(name: name, city: city, year: year)) { result, error in
if let _ = result?.data?.createConference {
self.presentingViewController?.dismiss(animated: true)
}
}
}
}
I also implemented cacheKeyForObject in AppDelegate like so:
apollo.cacheKeyForObject = { $0["id"] }
My question is whether it is possible to benefit from automatic UI updates with this setup? Currently when the CreateConferenceMutation is performed, the table view is not updated. Am I missing something or am I hitting the limitation that is mentioned in the docs:
In some cases, just using cacheKeyFromObject is not enough for your application UI to update correctly. For example, if you want to add something to a list of objects without refetching the entire list, or if there are some objects that to which you can’t assign an object identifier, Apollo cannot automatically update existing queries for you.

This is indeed a limitation of automatic UI updates. Although Apollo uses cacheKeyFromObject to match objects by ID, and this covers many common cases, it can't automatically update lists of objects.
In your schema, there is no way for Apollo to know that a newly added conference should be added to the allConferences list. All it knows is that allConferences returns a list of conference objects, but these could be arbitrarily selected and ordered.
So in cases like these, you will have to refetch the query from the server yourself, or change the mutation result to include the updated list.
Another option would be to manually add the new conference to the list in the client store. For this, the next version of Apollo iOS will include a manual update option similar to updateQueries in the Apollo JavaScript client.

Related

Core Data, CloudKit - Remove Duplicates Function deletes more than expected

I setup a little test app for CloudKit with preloaded Objects in Core Data.
When the app gets installed on multiple devices, CloudKit collects duplicate data, due to the preload.
I wrote some duplicate check functions to delete duplicates and relink relationships.
My test app hast three Entities. Book, Author, Pub(Publisher).
A Book has a relationship to many Authors and to one Publisher.
Publishers and Authors have relationships to many Books.
My duplicate checks actually work beside the fact that my Books have only one remaining Author (the first one) after the functions perform .
It's probably something very obvious but I'm scratching my head for a while now..
Does someone see something that could cause that?
Here is a comparison screenshot: (The data is fictive, I know that these are no books :D )
These are the three functions I perform accordingly to the duplicate data I detect.
private func remove(duplicatedBooks: [Book], winner: Book, performingContext: NSManagedObjectContext) {
for book in duplicatedBooks {
// Update Authors
if let authors = book.authors {
for case let author as Author in authors {
if let authorsBooks = author.books as? NSMutableSet {
if authorsBooks.contains(book) {
authorsBooks.remove(book)
authorsBooks.add(winner)
}
}
}
}
// Update Publishers
if let publisherBooks = book.publisher?.books as? NSMutableSet {
if publisherBooks.contains(book) {
publisherBooks.remove(book)
publisherBooks.add(winner)
}
}
performingContext.delete(book)
}
}
private func remove(duplicatedAuthors: [Author], winner: Author, performingContext: NSManagedObjectContext) {
for author in duplicatedAuthors {
// Update Books
if let books = author.books {
for case let book as Book in books {
if let bookAuthors = book.authors as? NSMutableSet {
if bookAuthors.contains(author) {
bookAuthors.remove(author)
bookAuthors.add(winner)
}
}
}
}
performingContext.delete(author)
}
}
private func remove(duplicatedPublishers: [Pub], winner: Pub, performingContext: NSManagedObjectContext) {
for pub in duplicatedPublishers {
// Update Books
if let books = pub.books {
for case let book as Book in books {
book.publisher = winner
}
}
performingContext.delete(pub)
}
}
That's an example of how I get to the remove functions.
The duplicateBooks in this case come from a fetch request that compared the names of the books and therefore decided that they are duplicates.
if var duplicateBooks = duplicateData as? [Book] {
let winner = duplicateBooks.first!
duplicateBooks.removeFirst()
remove(duplicatedBooks: duplicateBooks, winner: winner, performingContext: performingContext)
}
Yep, it was as obvious and stupid as I thought.
Instead of removing the duplicate and adding the remaining object as a relationship, I only removed and added it to the set.
It was a coincidence that only the first author remained.
Instead of
for case let author as Author in authors {
if let authorsBooks = author.books as? NSMutableSet {
if authorsBooks.contains(book) {
authorsBooks.remove(book)
authorsBooks.add(winner)
}
}
}
It should have been
if let authors = book.authors as? Set<Author> {
for author in authors {
author.removeFromBooks(book)
author.addToBooks(winner)
}
}

Realm - list of objects in an object (Swift 3)

I have a Models class defined like this:
class BaseModel: Object {
var data: JSON = JSON.null
convenience init(_ data: JSON) {
self.init()
self.data = data
}
override static func ignoredProperties() -> [String] {
return ["data"]
}
}
class RecipeModel: BaseModel {
dynamic var title: String {
get { return data["fields"]["title"].stringValue }
set { self.title = newValue }
}
... more vars ...
var ingredients: List<IngredientsModel> {
get {
let ingredients = List<IngredientsModel>()
for item in data["fields"]["ingredients"] {
ingredients.append(IngredientsModel(item.1))
}
return ingredients
}
set { self.ingredients = newValue }
}
}
class IngredientsModel: BaseModel {
dynamic var text: String {
get { return data["text"].stringValue }
set { self.text = newValue }
}
... more vars ...
}
And I would like to use it something like this:
Api.shared.fetchAllEntries().call(onSuccess: {response in
print(response.json)
let realm = try! Realm()
try! realm.write {
realm.deleteAll()
}
for item in response.json["items"].arrayValue {
let recipe = RecipeModel(item)
try! realm.write {
realm.add(recipe)
}
}
}, onError: {
print("error")
})
So basically the idea is to just pass the whole JSON to the initial RecipeModel class, and it should parse it out and create the objects I need in the Realm database. It works quite well except for the nested list of IngredientsModel. They do not get added to the realm database.
What I see as a potential problem is that I call self.init() before I call self.data in the convenience init, but I do not see any way to work around this. Do you guys please know how I could achieve that also the IngredientsModel would have its contents set up properly and I would have a list of ingredients in the RecipeModel?
Your current implementation doesn't work, because you are not calling the getter/setter of ingredients in the init method of RecipeModel and hence the IngredientsModel instances are never persisted in Realm.
Moreover, using a computed property as a one-to-many relationship (Realm List) is a really bad idea, especially if you are parsing the results inside the getter for this property. Every time you call the getter of ingredients, you create new model objects instead of just accessing the existing ones that are already stored in Realm, but you are never deleting the old ones. If you were actually saving the IngredientsModel instances to Realm (which you don't do at the moment as mentioned above) you would see that your database is full of duplicate entries.
Your whole approach seems really suboptimal. You shouldn't store the unparsed data object in your model class and use computed properties to parse it. You should parse it when initializing your models and shouldn't store the unparsed data at all. You can use the ObjectMapper library for creating Realm objects straight away from the JSON response.

Proper use of Strategy Pattern for simple networking layer in Swift 3

Does the following code demonstrate proper use of Strategy design pattern for a simple networking layer in swift 3?
Some code smells I'm unsure about:
violates Single responsibiility principle. Each strategy class such as Find, has a method for a different type of implementation. This is because I could want to find an image, or a user, or a chatroom. which are stored at different nodes in Firebase. all these different find methods are clumped together in Find class.
At the call sight of a request, if I need to make multiple async request, I nest the next request call inside the closure of the call back. Is this Ok?
The request object allows access to every type of insert, and find method. so in my signup VC I could i have the option to download a chatroom. Is even having access to that kind of implementation bad?
I have posted the code below, and left out all the actual implementation for brevity.
Any tips or guidance is much appreciated!
// USE CASE: Would go in viewDidLoad of ViewController
func testMyRequest () {
let myRequest = Request(insert: Insert(), find: Find())
myRequest.find?.user(with: "id", handler: { (user) in
myRequest.find?.nearbyUsers(user: user, handler: { (users) in
// update collectionView datasource
})
})
}
// Is this protocol necessary?
protocol RequestProtocol {
// - Family of algorithms, related actions.
var insert: Insert? { get set }
var find: Find? { get set }
}
// ---------------------------
class Request: RequestProtocol {
var insert: Insert?
var find: Find?
init(insert: Insert?, find: Find?) {
self.insert = insert
self.find = find
}
}
// Use a singleton maybe for the classes below? Why wouldn't I?
class Insert {
init() { }
func user(_ user: User) {
// insert user to firebase implementation
}
func message(_ message: Message) -> Void {
// insert message to firebase impelmentation
}
func image(data: Data, user: User) {
// insert image to firebase impelmentation
}
}
class Find {
init() { }
func user(with id: String, handler: #escaping (_ user: User) -> Void ) {
// find user implementation
}
func allChatrooms(handler: #escaping ([Chatroom]) -> Void) {
// find all chatrooms implementation
}
func nearbyUsers(user: User, handler: #escaping ([User]) -> Void ) {
// find nearby Users relative to current User location implementation
}
// Private helper method
private func findChatPartners (currentUser: User, chatrooms: [Chatroom] ) -> Set<String> {
}
}

Add arrays to Realm with swift 3

I'm new in Realm and I tried to add an Array as I did with strings and I ended up with some errors. So after a little search I found out a solution:
class Sensors : Object {
dynamic var name = ""
dynamic var message = ""
var topic: [String] {
get {
return _backingNickNames.map { $0.stringValue }
}
set {
_backingNickNames.removeAll()
_backingNickNames.append(objectsIn: newValue.map({ RealmString(value: [$0]) }))
}
}
let _backingNickNames = List<RealmString>()
override static func ignoredProperties() -> [String] {
return ["topic"]
}
}
class RealmString: Object {
dynamic var stringValue = ""
}
This is working very good, now I want to add another array inside this class.
If someone knows any other ways to add arrays with realm please share it.
Thanks in advance
As a general rule it's way more efficient to use the one-to-many relationships provided by Realm instead of trying to emulate them by using arrays (Realm's collections are lazy, the objects contained are instantiated only when needed as opposed to plain Swift arrays).
In your case, if I understand correctly what you're trying to do, you want to add [RealmString] Swift arrays to the _backingNickNames list.
Why not use the append(objectsIn:) method of Realm's List class (see here), like this:
// Dog model
class Dog: Object {
dynamic var name = ""
dynamic var owner: Person?
}
// Person model
class Person: Object {
dynamic var name = ""
dynamic var birthdate = NSDate(timeIntervalSince1970: 1)
let dogs = List<Dog>()
}
let jim = Person()
let dog1 = Dog()
let dog2 = Dog()
// here is where the magic happens
jim.dogs.append(objectsIn: [dog1, dog2])
If you want to do the opposite (convert from a List to an Array) just do :
let dogsArray = Array(jim.dogs)
• • • • • • • •
Back to your own posted solution, you could easily refactor the model to accommodate this. Each Sensor object could have several Topic and several Message objects attached.
Just ditch the message and topic computed properties and rename topicV and messageV to topics and messages respectively. Also rename RealmString to Topic and RealmString1 to Message.
Now, you could easily iterate through the, say, topics attached to a sensor like this :
for topic in sensor1.topics { ... }
Or if you want to attach a message to a sensor you could do something like this (don't forget to properly add the newly created object to the DB first):
let message1 = Message()
message1.stringValue = "Some text"
sensor2.messages.append(message1)
So, no need to use intermediary Swift Arrays.
After testing I managed to add another array like that:
class Sensors : Object {
dynamic var type = ""
dynamic var name = ""
dynamic var badge = 0
var topic: [String] {
get {
return topicV.map { $0.stringValue }
}
set {
topicV.removeAll()
topicV.append(objectsIn: newValue.map({ RealmString(value: [$0]) }))
}
}
var message: [String] {
get {
return messageV.map { $0.stringValue1 }
}
set {
messageV.removeAll()
messageV.append(objectsIn: newValue.map({ RealmString1(value: [$0]) }))
}
}
let topicV = List<RealmString>()
let messageV = List<RealmString1>()
override static func ignoredProperties() -> [String] {
return ["topic", "message"]
}
}
class RealmString: Object {
dynamic var stringValue = ""
}
class RealmString1: Object {
dynamic var stringValue1 = ""
}
What bogdanf has said, and the way you've implemented it are both correct.
Basic value types aside, Realm can only store references to singular Realm Object objects, as well as arrays of Objects using the List type. As such, if you want to save an array of types, it's necessary to encapsulate any basic types you want to save (like a String here) in a convenience Realm Object.
Like bogdanf said, it's not recommended to convert Realm Lists to standard Swift arrays and back again, since you lose the advantages of Realm's lazy-loading features (which can cause both performance and memory issues), but memory issues can at least be mitigated by enclosing the code copying data out of Realm in an #autoreleasepool block.
class MyObject: Object {
dynamic var childObject: MyObject?
let objectList = List<MyObject>()
}
So in review, it's best practice to work directly with Realm List objects whenever possible, and to use #autoreleasepool any time you do actually want to loop through every child object in a Realm. :)

Realm notification to RX block

I would like to hide my Realm implementation and instead of working on RLMNotificationBlock I would like to use RXSwift. Below how my method looks like now (RLMNotificationBlock is a block that takes String and RLMRealm):
func addNotificationBlock(block: RLMNotificationBlock) -> RLMNotificationToken? {
let rlmObject = ...
return rlmObject.addNotificationBlock(block)
}
But I would like to switch to more reactive observer-pattern way. I looked at RxSwift docs and source code of rx_clickedButtonAtIndex, but I cannot figure out how I should put all these things together. I guess my code at the end would look like:
public var rx_realmContentChanged: ControlEvent<Int> {
let controlEvent = ControlEvent()
// My code go here
return controlEvent
}
I'm new with RXSwift and know only the basics. Any help will be appreciated.
There is an Rx Realm extension available on GitHub you can use: https://github.com/RxSwiftCommunity/RxRealm
It allows you to get an Observable out of a single Realm object or a Realm Collection. Here's an example from the README:
let realm = try! Realm()
let laps = realm.objects(Lap.self))
Observable.changesetFrom(laps)
.subscribe(onNext: { results, changes in
if let changes = changes {
// it's an update
print(results)
print("deleted: \(changes.deleted) inserted: \(changes.inserted) updated: \(changes.updated)")
} else {
// it's the initial data
print(results)
}
})
There is also an additional library especially built for binding table and collection views called RxRealmDataSources
If I understood you correctly, you just want to return Observable<RLMNotificationToken>
In this case you just need to do something like this
func addNotificationBlock(block: RLMNotificationBlock) -> Observable<RLMNotificationToken> {
return create { observer -> Disposable in
let rlmObject = ...
let token = rlmObject.addNotificationBlock(block)
// Some condition
observer.onNext(token)
// Some other condition
observer.onError(NSError(domain: "My domain", code: -1, userInfo: nil))
return AnonymousDisposable {
// Dispose resources here
}
// If you have nothing to dipose return `NopDisposable.instance`
}
}
In order to use it bind it to button rx_tap or other use flatMap operator

Resources