Design pattern for Realm persistence - ios

I'm using Realm in a App and I'm trying to abstract much as possible so that in the future I can swap database providers without too much change.
This pattern has worked well although I'm concerned about the following.
Is creating a new realm object everytime a overhead (My current understanding is that Realm Objects are cached internally)?
Are there any problems with the way I'm using Realm ?
Are there any better design patterns for my purpose ?
public struct BookDataLayer: BookDataLayerProvider {
func isBookAvailable(bookIdentifier: String) throws -> Bool {
let database = try getDatabase()
return !database.objects(Book).filter("identifier = %#", bookIdentifier).isEmpty
}
func createOrUpdateBook(bookIdentifier: String, sortIndex: Int) throws {
let book = Book()
Book.bookIdentifier = bookIdentifier
Book.sortIndex = sortIndex
try create(book, update: true)
}}
protocol BookDataLayerProvider : DataAccessLayer {
func isBookAvailable(bookIdentifier: String) throws -> Bool
func createOrUpdateBook(bookIdentifier: String, sortIndex: Int) throws n}
extension DataAccessLayer {
func getDatabase() throws -> Realm {
do {
let realm = try Realm()
// Advance the transaction to the most recent state
realm.refresh()
return realm
} catch {
throw DataAccessError.DatastoreConnectionError
}
}
func create(object: Object, update: Bool = false) throws {
let database = try self.getDatabase()
do {
database.beginWrite()
database.add(object, update: update)
// Commit the write transaction
// to make this data available to other threads
try database.commitWrite()
} catch {
throw DataAccessError.InsertError
}
}}
// Usage
let bookDataLayer = BookDataLayer()
bookDataLayer.isBookAvailable("4557788")
bookDataLayer.createOrUpdateBook("45578899", 10)

That's a completely solid design pattern. It's pretty common for developers to abstract the data layer APIs way from their code in case they need to switch it out.
In response to your questions:
You're correct. Realm object instances are internally cached, so you can easily call let realm = try! Realm() multiple times with very little overhead.
Unless you've found a specific reason, it's probably not necessary to call refresh() on the Realm instance every time you use it. Realm instances on the main thread are automatically refreshed on each iteration of the run loop, so you only need to call refresh() if you're expecting changes on a background thread, or need to access changes before the current run loop has completed.
'Better' design patterns is probably a matter of opinion, but from what I've seen from other codebases, what you've got there is already great! :)

Related

"Cannot modify managed RLMArray outside of a write transaction." in XCTest

I'm attempting to write some unit tests for my view models that interact with Realm. The logic works fine when run on device / simulator, but triggers a ""RLMException", "Cannot modify managed RLMArray outside of a write transaction" when unit tested.
My test case is as follows...
func testThatNewlyAddedPaymentsAreReturned() throws {
let payment = Payment(recipient: "Matt", amount: Decimal(1.0), date: Date(), note: "")
try model.addPayment(payment: payment) // Throws exception
XCTAssertTrue(model.payments?.contains(payment) ?? false)
}
In the test case above, the model variable is the view model class, which has a simple one line implementation...
func addPayment(payment: Payment) throws {
try self.budget?.addPayment(payment: payment)
}
This in turn calls through to the Budget class where the Realm interactions take place.
func addPayment(payment: Payment) throws {
let realm = try Realm()
try realm.write {
_payments.append(payment)
}
}
Note that contrary to the exception message the private var _payments = List<Payment>() property is being modified within a Realm write transaction.
I've configured the default Realm configuration in the unit test as follows...
override func setUp() {
var config = Realm.Configuration.init()
// Set this as the configuration used for the default Realm
Realm.Configuration.defaultConfiguration = config
config.inMemoryIdentifier = "BudgetTests"
try! repository = BudgetRepository.init(realm: Realm(configuration: config))
try! initialiseViewModel()
}
Updating the test setup to initialise the realm using the no-args initializer fixed the issue e.g.
Replacing...
try! repository = BudgetRepository.init(realm: Realm(configuration: config))
... with ...
try! repository = BudgetRepository.init(realm: Realm())
Oddly this would appear to imply that opening a Realm with the default configratuion e.g. Realm() does not produce the equivalent Realm as initializating one and manually supplying the default configuration.

How to use swift-4 Promises then, done, catch and other blocks

I would like to learn about promises in swift-4. How to use multiple then statements and done, catch blocks.
Here I am trying to get the value from the promise. But I'm getting errors. Could someone help me to understand promises?
Here is my code.
import UIKit
import PromiseKit
struct User {
var firstname : String?
var lastname : String?
}
struct APIError {
var message : String?
}
class ViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let userPromise : Promise = self.getUserDetails()
userPromise.then { user -> Void in
//print(user.f)
}
}
func getUserDetails()->Promise<User> {
return Promise<User> { resolve in
let user = User(firstname: "Scot", lastname: "dem")
if ((user.firstname?.count) != nil) {
resolve.fulfill(user)
} else {
let error = APIError(message: "User not valid")
resolve.reject(error as! Error)
}
}
}
}
Once I get the user details I want to make a full name, uppercase promises which are dependent on userPromise.
I would like to use multiple then, done, finally blocks. Just want to understand usage.
Why I'm getting an error here when we use userPromise.then { user -> Void in
what should I give inside the block
In PromiseKit 6, then can no longer return Void. This is mainly due to the tuplegate issue in Swift 4.
Quote from PromieKit 6 Release News
With PromiseKit our then did multiple things, and we relied on Swift
to infer the correct then from context. However with multiple line
thens it would fail to do this, and instead of telling you that the
situation was ambiguous it would invent some other error. Often the
dreaded cannot convert T to AnyPromise. We have a troubleshooting
guide to combat this but I believe in tools that just work, and when
you spend 4 years waiting for Swift to fix the issue and Swift doesn’t
fix the issue, what do you do? We chose to find a solution at the
higher level.
So we split then into then, done and map.
then is fed the previous promise value and requires you return a promise.
done is fed the previous promise value and returns a Void promise (which is 80% of chain usage)
map is fed the previous promise value and requires you return a non-promise, ie. a value.
Hence .then { (user) -> Void in is no longer valid and that's why you're getting an error.
It's now designed to return a Thenable like so:
userPromise.then { user -> Promise<User> in
//return yet another promise
}
The .then that used to return Void is now it's own .done.
i.e:
userPromise.done { (user) in
print(user)
}
So when you mix 'em up:
We get:
userPromise
.then { (user) -> Promise<User> in
//return another Promise
return self.uppercasePromise(on: user)
}
.done { (user) in
/*
Depending on your sequence, no more promises are left
and you should have a matured user object by now
*/
print(user)
}
.catch { (error) in
print(error)
}
.finally {
print("finally")
}
func uppercasePromise(on user: User) -> Promise<User> {
/*
Didn't understand your statement but do whatever you meant when you said:
"Once I get the user details I want to make a full name, "
uppercase promises which are dependent on userPromise."
*/
return Promise { seal in
seal.fulfill(user)
}
}

About transaction in Realm database

This is my Realm object:
class AchievementDate : Object {
dynamic var date: Date = Date()
dynamic var apple: Int = Int(0)
func save() {
do {
let realm = try Realm()
try realm.write {
realm.add(self)
}
} catch let error as NSError {
fatalError(error.localizedDescription)
}
}
}
I change apple's value in View controller's viewDidLoad() method, as you can see:
override func viewDidLoad() {
super.viewDidLoad()
achievementDate.apple = 2
achievementDate.save()
}
then I update the apple's value when user clicks the pause button on the screen like this:
#IBAction func pausedButtonTapped(_ sender: UIButton) {
achievementDate.apple += 1
achievementDate.save()
}
Xcode runs it successfully but when I click the pause button, the app crashes. In the console it says:
*** Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call
beginWriteTransaction on an RLMRealm instance first.'
I am quite confused about this, btw, what does transaction mean in general? Thanks a lot.
A write transaction is used to group modifications of objects within a Realm into a single unit of work. Managed Realm objects may only be modified within a write transaction. The write transaction is scoped to the block you pass to the call to Realm.write(_:). The call to write begins a write transaction, the body is executed with the transaction active, and when the block returns the write transaction is committed and the changes are persisted to the Realm file.
You've not shared how achievementDate is initialized, but it seems safe to assume based on the exception you're seeing that it is an AchievementDate instance that is a managed object (that is, it was either created then added to a Realm, or it was loaded from a Realm). As the exception notes, you can only modify managed objects within a write transaction. You can either expand the scope of your write transaction to encompass the modification of the managed object, or you can avoid modifying a managed object altogether (by adding a primary key to your model class, and using either Realm.create(_:value:update:) or Realm.add(_:update:) with update: true to update an existing object with the given primary key value).
First, you need to define a Primary Key if you want to update the model:
class AchievementDate: Object {
dynamic var id = 0
dynamic var date:Date = Date()
dynamic var apple:Int = Int(0)
override static func primaryKey() -> String? {
return "id"
}
}
Then update the model like:
static func save(achievementDate:AchievementDate) {
let realm = try! Realm()
try! realm.write {
realm.add(achievementDate, update: true)
}
}
Alternatively, if you don't want primary Key, and the model is already retrieved from Realm, you could update the model like this:
let realm = try! Realm()
try! realm.write {
achievementDate.apple = 2
}

Realm and Parse integration: how to save the realm

I'm trying to create a bundle realm for my application. I thought it should be quite simple. Since I already have all needed records in Parse, I just have to:
create realm models and objects
load parse records to realm objects
save the realm
So, I created two realm models:
class RealmContainer : Object {
dynamic var rContainerId: String! //should be equal objectId from Parse
dynamic var rContainerName: String! //should be equal "name" field from Parse
...
var messages = List<RealmMessage>()
override static func primaryKey() -> String? {
return "rContainerId"
}
}
and
class RealmMessage : Object {
dynamic var rMessageId: String!
...
dynamic var rParentContainer: RealmContainer!
}
Getting results from Parse seems to be working. Also my realm objects are also good
var allUserContainers: [RealmContainer] = []
I was able to populate this array with values from Parse. But when I'm trying to save this realm, I'm getting a) nothing or b) error message
My code (this one'll get nothing):
let realm = try! Realm()
try! realm.write {
realm.add(self.allUserContainers[0])
print(Realm().path)
print(realm.path)
}
My code (this one'll get nothing too):
let realm = try! Realm()
try! realm.write {
realm.create(RealmContainer.self, value: self.allUserContainers[0], update: true)
print(Realm().path)
print(realm.path)
}
My code 3 (this will get me an error message "Terminating app due to uncaught exception 'RLMException', reason: 'Illegal recursive call of +[RLMSchema sharedSchema]. Note: Properties of Swift Object classes must not be prepopulated with queried results from a Realm"):
//from firstViewController, realm is a global variable
let realm = try! Realm()
//another swift module
try! realm.write {
realm.create(RealmContainer.self, value: self.allUserContainers[0], update: true)
print(Realm().path)
print(realm.path)
}
Obviously I don't properly understand how it should work, but I tried several swift/realm tutorials and they were actually straightforward. So, what did I do wrong?
Update
So, I updated my code to make it as much simple/readable as possible. I have a Dog class, and I am trying to get Dogs from Parse and put them to Realm.
AppDelegate.swift
let realm = try! Realm() //global
Dog.swift
class Dog : Object {
dynamic var name = ""
dynamic var age = 0
}
User.swift (getDogs and putDogs functions)
class User {
var newDogs:[Dog] = []
...
func getDogs() {
self.newDogs = []
let dogsQuery = PFQuery(className: "Dogs")
dogsQuery.limit = 100
dogsQuery.findObjectsInBackgroundWithBlock { (currentModes, error) -> Void in
if error == nil {
let tempModes:[PFObject] = currentModes as [PFObject]!
for var i = 0; i < tempModes.count; i++ {
let temp = Dog()
temp.name = currentModes![i]["dogName"] as! String
self.newDogs.append(temp)
}
} else {
print("something happened")
}
}
}
...
func putDogs() {
print(self.newDogs.count)
try! realm.write({ () -> Void in
for var i = 0; i < newDogs.count; i++ {
realm.add(newDogs[i])
}
})
try! realm.commitWrite() //doesn't change anything
}
error message still the same:
Terminating app due to uncaught exception 'RLMException', reason:
'Illegal recursive call of +[RLMSchema sharedSchema]. Note: Properties
of Swift Object classes must not be prepopulated with queried
results from a Realm
I believe I just have some global misunderstanding about how Realm is working because it is extremely simple configuration.
About your RealmSwift code : You have implemented it right.
When you declare a class of realm in swift, it's a subclass of Object class. Similarly for the parse it's subclass of PFObject.
You custom class have to have only one base class. So can't use functionalities of both the libraries Parse as well as Realm.
If you have to have use both Parse and Realm, you need to declare two different classes like RealmMessage and ParseMessage.
Retrieve data for ParseMessage from parse and copy properties to RealmMessage.
Why you want to use both Parse and Realm ?
parse also provides local data store by just writing Parse.enableLocalDatastore() in appDelegate before
Parse.setApplicationId("key",
clientKey: "key")
Your Realm Swift code seems fine. Realm is very flexible when creating objects using (_:value:update:), in being able to take in any type of object that supports subscripts. So you could even directly insert PFObject if the schemas matched.
The problem seems to be how you're populating the allUserContainers array. As the error says, you cannot fetch a Realm Object from Realm and then try and re-add it that way. If you're trying to update an object already in Realm, as long as the primary key properties match, you don't need to supply the whole object back again.
Can you please revisit the logic of how your allUserContainers variable is being populated, and if you can't fix it, post the code into your question?
Sidenote: You probably don't need to define your Realm properties as implicitly unwrapped as well. This is the recommended pattern:
class RealmContainer : Object {
dynamic var rContainerId = ""
dynamic var rContainerName = ""
}
Actually I found what was wrong: as i suspected it was a stupid mistake, some property in my class was UIImage type, and Realm just can't work with this type (even if I'm not trying to put objects of this class into realm). So, my bad. I am slightly embarassed by it, but will not delete my question because error messages from Realm were not very understandable in this case

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