Realm-iOS: Model object becomes nil when it's added to realm - ios

It's an RSS reader app. I instantiate my model object, call setupFavIcon() on it to download favIcon which is nil when creating an object. Then I add each object to realm. But when the icon is actually fetched, self is nil, so I can't update app's UI. I don't understand why self becomes nil.
class Article: Object {
dynamic var source = ""
dynamic var title = ""
dynamic var link = ""
dynamic var pubDate = Date()
dynamic var favIcon: Data?
dynamic var favIconDidLoad: (() -> ())?
func setupFavIcon(_ source: String) {
DownloadManager.sharedInstance.downloadFavIcon(source) { [weak self] icon in
if let icon = icon {
self?.favIcon = icon
self?.favIconDidLoad?()
}
}
}
override class func primaryKey() -> String? {
return "link"
}
override class func ignoredProperties() -> [String] {
return ["favIconDidLoad"]
}
}
Closure favIconDidLoad is defined in my TableViewCell class and invoked when favIcon is downloaded.
fileprivate func setupFavIcon(_ article: Article) {
if let favicon = article.favIcon {
setFavIcon(favicon)
} else {
article.favIconDidLoad = { [weak self] in
self?.setFavIcon(article.favIcon)
}
}
}

You should keep strong reference to your Article objects. When you load them from realm you need keep them in array. if you operate on results from realm they are released when method TableViewCell setupFavIcon ends.

You use weak reference to self in downloadFavIcon closure that most likely is called asynchronously, so your objects could be already deallocated. You need to use strong reference in closure or keep strong references to your objects somewhere.
Also note: if object is added to Realm, all changes to an object (addition, modification and deletion) must be done within a write transaction.

Related

iOS private/public data management and storage in Swift

Is it possible to display as one array objects retrieved from network and same model but retrieved from core data. Purpose is to have same data possible to be public (then retrieved from network) or private and then this data is stored locally in coredata model. Attributes/Properties will be the same for both.
I plan to display this as swiftUI view (if that matters)
After some search I came with idea to have one struct that based on its privacy property will be translated to core data class model or if public directly connected to networking layer?
for example (some pseudo swift ;) )
struct Note {
let note: String
let isPrivate: Bool
func save(self) {
if self.isPrivate { save to CoreData }
else { send save request with use of networking }
}
}
class coreDataModel: NSManagedObject {
var note: String
let isPrivate = true
}
struct networkingModel {
var note: String
let isPrivate = false
}
class modelManager {
func joinData() {
let joinedModel: Note = coreDataModel + networkingModel
// and so on to display that model
}
}
I think you can try this, first load the ui with the existing local data. At the same time, make a call to your api on a background queue. Once the api results are available, filter for duplicates, then persist it locally. Then the last step is to notify the ui to reload.
This pseudo-code is a bit UIKit specific, however the same logic can be applied to when in SwiftUI. You won't need closure, you will compute directly to your publisher object, and ui will react based any new emits.
/// you can have one model for both api and core data
struct Note: Hashable { let uuid = UUID.init() }
class ModelManager {
private var _items: Array<Note> = []
var onChanged: ((Array<Note>) -> Void)? = nil
// you might also have an init, where you can have a timer running
func start() {
loadFromCoreData { [weak self] (items) in
guard let `self` = self else { return }
self._items.append(contentsOf: items)
self.onChanged?(self._items)
self.loadFromApi()
}
}
private func loadFromCoreData(completion: #escaping (Array<Note>) -> Void) {
// background queue
// logic to load from coredata.
let coreDataResults: Array<Note> = []
completion(coreDataResults)
}
private func loadFromApi() {
// background queue
let apiResults: Array<Note> = []
compute(contents: apiResults)
}
private func compute(contents: Array<Note>) {
let combined = zip(_items, contents).flatMap{[$0.0, $0.1] }
let newItems = Array(Set(combined)) // set doesn't allow duplicates
// save to db
// insert to new to _items
_items.append(contentsOf: newItems)
// sort maybe to place new items on top
self.onChanged?(self._items)
}
}
// usage of this object
let manager = ModelManager()
manager.start()
manager.onChanged = { items in
// [weak self] remember of retain cycles
// make sure you are on main queue when reloading
}

Realm Swift - save a reference to another object

I thought this would be pretty straightforward after reading here and here but I'm a bit stuck.
I have a 'favouriteWorkout' object that looks like this :
class FavouriteObject: Object {
#objc dynamic var favouriteWorkoutName = ""
#objc dynamic var workoutReference = WorkoutSessionObject()
override class func primaryKey() -> String? {
return "favouriteWorkoutName"
}
}
What I'm trying to do here is reference a WorkoutSessionObject in Realm that links from a WorkoutName when a workout is saved as a favourite.
My WorkoutSessionObject has a primary key of workoutID which is a UUID string. It looks like this :
class WorkoutSessionObject: Object {
#objc dynamic var workoutID = UUID().uuidString
#objc dynamic var workoutType = ""
let exercises = List<WorkoutExercise>()
#objc dynamic var totalExerciseCount = 0
#objc dynamic var rounds = 0
#objc dynamic var favourite : Bool = false
override class func primaryKey() -> String? {
return "workoutID"
}
}
I've then tried to save using this :
let favouriteWorkout = FavouriteObject()
favouriteWorkout.favouriteWorkoutName = favouriteName
favouriteWorkout.workoutReference = (realm.object(ofType: WorkoutSessionObject.self, forPrimaryKey: self.workoutID))!
do {
try realm.write {
realm.add(favouriteWorkout)
}
} catch {
print ("Error adding favourite")
}
but i get a crash when I run of :
'RLMException', reason: 'The FavouriteObject.workoutReference property must be marked as being optional.
However, when I then try to make it optional (by adding ?) it says
"Cannot use optional chaining on non-optional value of type 'WorkoutSessionObject"!
Summary
I want to save a reference of the workoutID of a WorkoutSessionObject in my FavouriteObject which is an actual link to the WorkoutSessionObject (so the properties can be accessed from favourites)
Update
using the answers below I've now sorted the problem of the workout reference. This is now showing in Realm as the proper format () under "workoutReference". However, I'm now getting "nil" in "workoutReference" when trying to save. I know the workoutID is coming through correctly as I am printing it in the console.
You need to change the declaration of workoutReference. First of all, you need to make it Optional by writing ? after the type. Secondly, you shouldn't assign a default value to it, it needs to be Optional for a reason. The linked docs clearly state that
to-one relationships must be optional
, and workoutReference is clearly a to-one relationship.
class FavouriteObject: Object {
#objc dynamic var favouriteWorkoutName = ""
#objc dynamic var workoutReference:WorkoutSessionObject?
override class func primaryKey() -> String? {
return "favouriteWorkoutName"
}
}
In property-cheatsheet you can see that a non-optional Object-property is not allowed, so you have to change it like the following:
class FavouriteObject: Object {
#objc dynamic var favouriteWorkoutName = ""
// here you have to make the property optional
#objc dynamic var workoutReference: WorkoutSessionObject?
override class func primaryKey() -> String? {
return "favouriteWorkoutName"
}
}

Add a non realm object as ignored property to realm object in swift?

I am trying to add a non-realm class object to realm object something like this.
class TrainTripItinerary: Object {
dynamic var departStationName: String?
dynamic var departStationCode: String?
var runningStatus: TrainRunningStatus?
override static func ignoredProperties() -> [String] {
return ["runningStatus"]
}
}
While TrainRunningStatus is not a realm class.
class TrainRunningStatus {
var trainDataFound: String?
var startDate: String?
var startDayDiff: String?
}
I am not able to update runningstatus property now. Anyone know how it works? I fetch separately runnningstatus and assign it to the realm object later but it stays nil even after the assignment.
eg.
let runningStatus = TrainRunningStatus()
trainTripItinerary.runningStatus = runningStatus
This line is not working, trainTripItinerary runningStatus property is not set properly its always nil.
As suggested in comments make sure you use the same instance of TrainTripItinerary because ignored properties won’t automatically update their value across different instances.
See an example code below that demonstrates how ignored properties work
let realm = try! Realm()
try! realm.write {
realm.deleteAll()
}
let runningStatus = TrainRunningStatus()
var trainTripItinerary = TrainTripItinerary()
trainTripItinerary.runningStatus = runningStatus
assert(trainTripItinerary.runningStatus != nil)
try! realm.write {
realm.add(trainTripItinerary);
}
assert(trainTripItinerary.runningStatus != nil)
trainTripItinerary = realm.objects(TrainTripItinerary.self).first!
assert(trainTripItinerary.runningStatus == nil)
Firstly, your code is not correct.
class TrainTripItinerary: Object {
dynamic var departStationName: String?
dynamic var departStationCode: String?
var runningStatus: TrainRunningStatus?
override static func ignoredProperties() -> [String] {
return ["runningStatus"]
}
}
func ignoredProperties() -> [String] is only used on Realm properties. Since your property var runningStatus: TrainRunningStatus? does not begin with dynamic, it is not a Realm property. You don't need to use func ignoredProperties() -> [String] here.
var runningStatus: TrainRunningStatus? here is called a "transient property" in Realm. Usually a transient property is something calculated basing on current date or on Realm properties, Realm won't do anything on transient properties and you should maintain them yourself.
So if you just want to use runningStatus as a transient property, you can simply remove the code override static func ignoredProperties() -> [String].

Realm object as member is nil after saving

I'm facing an issue where a Realm object has another Realm object as member which is always nil after adding to the database.
class MedPack: Object {
dynamic var uuid = NSUUID().UUIDString
dynamic var medicine: Medicine?
convenience init(medicine: Medicine) {
self.init()
self.medicine = medicine
}
override static func primaryKey() -> String? {
return "uuid"
}
}
The reference to the object Medicine is always nil after adding.
class Medicine: Object {
var uuid = NSUUID().UUIDString
var name: String?
override static func primaryKey() -> String? {
return "uuid"
}
}
Creation of object
let medPack = MedPack(medicine: med)
Adding to database
static let sharedInstance = DBHelper()
var realmDb: Realm!
private init() {
realmDb = try! Realm()
}
func store(object: Object) {
try! self.realmDb.write {
self.realmDb.add(object)
}
}
After comparing this code to one of the Realm sample projects, it would appear that simply setting an Object as a child of another does not implicitly write it to the database as well.
Instead, you may need to refactor your code slightly, but make sure you explicitly add your Medicine object to Realm in a write transaction, before you set its relation to MedPack and then write MedPack to the database.

Realm Swift EXC_BAD_ACCESS During Segue with Unpersisted Object

I've got an app that I've started adding Realm to and I think I must be doing something wrong because I keep running into EXC_BAD_ACCESS when passing unpersisted objects between view controllers.
Here's a stripped down version of my app.
class TodoTask: Object {
dynamic var name: String = ""
let steps = List<Step>()
convenience init(name: String) {
self.init()
self.name = name
}
}
class Step: Object {
dynamic var name: String = ""
convenience init(name: String) {
self.init()
self.name = name
}
}
class TodoListController: UIViewController {
let todos = List<TodoTask>()
override func viewDidLoad() {
super.viewDidLoad()
var t1 = TodoTask(name: "Todo1")
let steps = [
Step("Do this"),
Step("Do that"),
]
t1.steps.appendContentsOf(steps)
var t2 = TodoTask(name: "Todo2")
let steps = [
Step("Do these"),
Step("Do those"),
]
t2.steps.appendContentsOf(steps)
todos.appendContentsOf([t1, t2])
// Display table of todos.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let detailsController = segue.destinationViewController as? TodoDetailViewController,
let selectedTask = getSelectedTask() {
detailsController.task = selectedTask
}
}
}
class TodoDetailViewController: UIViewController {
var task: TodoTask? // <<< EXC_BAD_ACCESS
// Display the task's steps.
}
Unfortunately, I can't figure out what triggers the EXC_BAD_ACCESS and it happens intermittently. I didn't copy a stacktrace (d'oh!) but I remember it being in the C++ destructor for some sort of Row object. This is odd to me because there doesn't seem to be a database file in my Documents folder.
I'm pretty confident this is a Realm-based error because I experienced no weird crashes until I converted my plain Swift objects to Realm objects.
My only hunch is that I might be using List wrong since the warning This method can only be called during a write transaction. is in the comments of the appendContentsOf method. However, I was under the impression that I could use Realm objects that weren't stored in a Realm just like normal Swift objects.
Am I using Realm objects wrong? Is there anything else I can look into?
I'm on Realm 0.95.2.
Thanks!
Edit:
func getSelectedTask() -> TodoTask? {
if let index = taskTableView.indexPathForSelectedRow {
return tasks[index]
}
return nil
}
The issue is that a Realm List should not be created directly. This class is only used to manage to-many relationships on Object models. Instead, you should use a standard Swift array to store the unpersisted Realm Objects.
So switch:
let todos = List<TodoTask>()
to
let todos = [TodoTask]()

Resources