realm updating data + DispatchQueue - ios

I have update DataBase in background. My data can contain ~2000 items and it take time to update.
func updateData(items: [JSON], _ complete:#escaping() -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let currentModels = EgrnModel.getAllModels()
var newModels: [EgrnModel] = []
var toDelete: [EgrnModel] = []
for model in currentModels {
let contain = items.contains(where: {$0["id"].intValue == model.id})
if !contain {
toDelete.append(model)
}
}
let realm = try! Realm()
try! realm.write {
for item in items {
if let model = currentModels.first(where: {$0.id == item["id"].intValue}) {
model.update(item)
}
else {
newModels.append(EgrnModel(item))
}
}
realm.delete(toDelete)
realm.add(newModels)
}
DispatchQueue.main.async {
complete()
}
}
}
and I have a function in which I need update data momentarily. When I tap checkmark I have a freeze. (I think it because at this time other data is updating in background)
func checkMark(index: Int) {
let model = models[index]
let realm = try! Realm()
try! realm.write {
model.needToUpdateOnServer = true
model.lastEditUpdate = Date()
model.read = true
}
}
I try next code to fix a freeze. But in this code I have a crash Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.
func checkMark(index: Int) {
let model = models[index]
DispatchQueue.global(qos: .userInitiated).async {
let realm = try! Realm()
try! realm.write {
model.needToUpdateOnServer = true
model.lastEditUpdate = Date()
model.read = true
}
}
}

What you need to is "move" realm objects from one thread to another because realm objects are not thread-safe but Thread Confined. To achieve this you have to use ThreadSafeReference API.
To solve this problem do the following:
Create an extension on the realm class
extension Realm {
func writeAsync<T : ThreadConfined>(obj: T, errorHandler: #escaping ((_ error : Swift.Error) -> Void) = { _ in return }, block: #escaping ((Realm, T?) -> Void)) {
let wrappedObj = ThreadSafeReference(to: obj)
let config = self.configuration
DispatchQueue(label: "background").async {
autoreleasepool {
do {
let realm = try Realm(configuration: config)
let obj = realm.resolve(wrappedObj)
try realm.write {
block(realm, obj)
}
}
catch {
errorHandler(error)
}
}
}
}
}
Use it in your code this way
func checkMark(index: Int) {
let model = models[index]
let realm = try! Realm()
realm.asyncWrite(model) { realm, model in
model.needToUpdateOnServer = true
model.lastEditUpdate = Date()
model.read = true
}
}
HAPPY SWIFTING!

Related

How to save normal array into realm in background thread swift

I have a RealmManager class as bellow:
class RealmManager {
static func save<T: Object>(obj: [T]) {
let personRef = ThreadSafeReference(to: obj)
DispatchQueue.global(qos: .background).async {
autoreleasepool {
let realm = try! Realm()
try! realm.write({
realm.add(obj, update: .modified)
})
}
}
}
}
I'm trying to save a obj array into Realm database but my code do not compile getting this error:
Referencing initializer 'init(to:)' on 'ThreadSafeReference' requires
that '[_]' conform to 'ThreadConfined'
Any help?
Code update:
class RealmManager {
static func save<T: Object>(obj: [T]) {
let realm = try! Realm()
realm.asyncSaveArray(obj: obj)
}
static func get<T: Object>(type: T.Type) -> [T]? {
let realm = try! Realm()
return realm.objects(T.self).toArray(type: T.self)
}
}
extension Results {
func toArray<T>(type: T.Type) -> [T] {
return compactMap { $0 as? T }
}
}
extension Realm {
func asyncWrite<T : ThreadConfined>(obj: T, errorHandler: #escaping ((_ error : Swift.Error) -> Void) = { _ in return }, block: #escaping ((Realm, T?) -> Void)) {
let wrappedObj = ThreadSafeReference(to: obj)
let config = self.configuration
DispatchQueue.global(qos: .background).async {
autoreleasepool {
do {
let realm = try Realm(configuration: config)
let obj = realm.resolve(wrappedObj)
try realm.write {
block(realm, obj)
}
}
catch {
errorHandler(error)
}
}
}
}
func asyncSaveArray<T: Object>(obj: [T]) {
for item in obj {
self.asyncWrite(obj: item) { (realm, itemToSave) in
guard let itemToSave = itemToSave else { return }
realm.add(itemToSave, update: .modified)
}
}
}
}
To pass in an array of objects, you might use a Realm extension:
extension Realm {
  func asyncWrite<T : ThreadConfined>(obj: T, errorHandler: #escaping ((_ error : Swift.Error) -> Void) = { _ in return }, block: #escaping ((Realm, T?) -> Void)) {
    let wrappedObj = ThreadSafeReference(to: obj)
    let config = self.configuration
    DispatchQueue.global(qos: .background).async {
      autoreleasepool {
        do {
          let realm = try Realm(configuration: config)
          let obj = realm.resolve(wrappedObj)
           
          try realm.write {
            block(realm, obj)
          }
        }
        catch {
          errorHandler(error)
        }
      }
    }
  }
   
  func asyncSaveArray<T: Object>(obj: [T]) {
     
    for item in obj {
      self.asyncWrite(obj: item) { (realm, itemToSave) in
        guard itemToSave != nil else { return }
        realm.add(itemToSave!, update: .modified)
      }
    }
  }
}
There might be two possible use cases.
One is to actually pass an array of Objects:
realm.asyncSaveArray(obj: yourObjectsArray)
The other one is instead of passing in the array of objects, you can use an instance of Results<Object> received somewhere in code previously:
var yourObjects = realm.objects(YourType.self)
self.asyncWrite(obj: yourObjects) { (realm, itemToSave) in
        guard itemToSave != nil else { return }
        realm.add(itemToSave!, update: .modified)
}

Problems with CoreData

Now I do my RSSReader app and need to add CoreData on it.
I use pod FeedKit, that have class RSSFeed which receive data from chanel. That data I need to save to CoreData and then display on my app. I have DataManager that has method saveChanel from CoreData.
My data I get on DataManager. DataManager have method loadChanel that have method saveChanel from CoreData (PersistanceManager.shared.saveChanell(feed: RSSFeed)).
My coreDataModel has 2 entity "Item" and "Chanel".
And I know that I have problems in my code on PersistanceManager(code below). Can somebody help with it?
My DataManager is:
class DataManager {
static let sharedInstance = DataManager()
// var dataManagerDelegate: DataManagerDelegate?
var myLinkString: String!
private init() {}
// MARK: - Public
func loadChanel(completion: #escaping ([Chanel]?, Error?, Bool) -> Void, channelAddres: String) {
guard let myLink = myLinkString, let linkUrl = URL(string: myLink) else { return }
let parser = FeedParser(URL: linkUrl)
let result = parser.parse()
guard let feed = result.rssFeed, result.isSuccess else { return }
PersistanceManager.shared.saveChanell(feed: RSSFeed)
}
func loadChannels(completion: #escaping ([Chanel]?, Error?, Bool) -> Void) {
func performCompletion(channels: [Chanel]?, error: Error?, finished: Bool) {
DispatchQueue.main.async {
completion(channels, error, finished)
}
}
/*
* Fetch local channels
*/
let cachedChannels = PersistanceManager.shared.fetchRssChannels()
performCompletion(channels: cachedChannels, error: nil, finished: false)
/*
* Load channels from server
*/
myLinkString = "https://..."
guard let linkUrl = URL(string: myLinkString) else { return }
let parser = FeedParser(URL: linkUrl)
let result = parser.parse()
guard let feed = result.rssFeed, result.isSuccess else { return }
/*
* Store data to database
*/
PersistanceManager.shared.saveChanell(channel: Chanel, feed: RSSFeed)
/*
* Get actual posts from data base and return
*/
let updatedChannels = PersistanceManager.shared.fetchRssChannels()
performCompletion(channels: updatedChannels, error: nil, finished: true)
}
func saveContext() {
PersistanceManager.shared.saveContext()
}
}
On DataManager I have 3 errors on the next lines:
PersistanceManager.shared.saveChanell(feed: RSSFeed)
PersistanceManager.shared.saveChanell(channel: Chanel, feed: RSSFeed)
Errors are:
"Cannot convert value of type 'RSSFeed.Type' to expected argument type 'RSSFeed'");
"Cannot convert value of type 'Chanel.Type' to expected argument type 'Chanel'"
And PersistanceManager:
import Foundation
import CoreData
import FeedKit
class PersistanceManager {
// wrapper for core data
static var shared = PersistanceManager()
private init() {}
func saveChanell(feed: RSSFeed) {
let channel = createNewChanel(with: feed.title)
channel?.rssDescription = feed.description
channel?.pubdate = feed.pubDate! as NSDate
channel?.link = feed.link
channel?.language = feed.language
channel?.creator = feed.dublinCore?.dcCreator
guard let feedItems = feed.items else {
return
}
guard let _ = channel?.item else {
channel?.item = NSSet()
}
guard let channelItems = channel?.item else {
return
}
for feedItem in feedItems {
}
// 5
do {
try context.save()
print("Channel saved")
} catch {}
}
func saveChanell(channel: Chanel, feed: RSSFeed) {
channel.title = feed.title
channel.rssDescription = feed.description
channel.pubdate = feed.pubDate! as NSDate
//channel.pubDate = NSDate(timeIntervalSince1970: feed.pubDate?.timeIntervalSince1970)
channel.link = feed.link
channel.language = feed.language
//channel.isLastUsed
channel.creator = feed.dublinCore?.dcCreator
guard let items = feed.items else {
return
}
// 3
for item in items {
guard let mediaLink = item.media?.mediaThumbnails?.first?.attributes?.url else {
continue
}
let rssItem = createRssItem(with: mediaLink, in: context)
rssItem?.title = item.title
// rssItem?.pubdate = (item.pubDate as! NSDate)
rssItem?.link = item.link
rssItem?.itemDescription = item.description
rssItem?.category = item.categories?.first?.value
channel.item?.adding(rssItem as Any)
}
// 5
do {
try context.save()
print("Channel saved")
} catch {}
}
// 4
private func createRssItem(with mediaLink: String, in context: NSManagedObjectContext) -> Item? {
let newRssItem = NSEntityDescription.insertNewObject(forEntityName: "Item", into: context) as? Item
newRssItem?.mediaLink = mediaLink
print("RSS Item created")
return newRssItem
}
func fetchRssChannels() -> [Chanel]? {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Chanel")
do {
let channels = try self.context.fetch(fetchRequest) as? [Chanel]
return channels
} catch let error {
print(error)
}
print("RSS channels fetched")
return nil
}
// 2 - а тот ли тут канал
func createNewChanel(with chanel: Chanel) -> Chanel? {
if let findRssChannelCD = findRssChannel(title: chanel.title) {
print("findRssChannelCD")
return findRssChannelCD
}
let newRssChannelCD = NSEntityDescription.insertNewObject(forEntityName: "Chanel", into: context) as? Chanel
newRssChannelCD?.title = chanel.title
newRssChannelCD?.item = NSSet()
do {
try context.save()
print("newChanelSaved")
} catch {}
return newRssChannelCD
}
// 1
private func findRssChannel(title: String?) -> Chanel? {
guard let title = title else {
return nil
}
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Chanel")
request.predicate = NSPredicate(format: "title = %#", title)
do {
let users = try context.fetch(request) as? [Chanel]
return users?.first
} catch {}
return nil
}
// MARK: - Core Data stack
var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "NewsForIphone")
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
//You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
public func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
//You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
And I know that I have problems in my code on PersistanceManager(code below), and errors in DataManager. Can somebody help with it?
The issue is caused by this line:
PersistanceManager.shared.saveChanell(feed: RSSFeed)
You are specifying actual class name instead of the object, you need to write:
PersistanceManager.shared.saveChanell(feed: feed)

DispatchGroup and Realm

My application uses Swift 3.1, Realm 2.7.0 as database and has a background service that uses the DispatchGroup to control my flow of a determined process.
First things first, in my ViewController I did an implementation of the Realm's notification system, knows as NotificationToken, that uses the method addNotificationBlock to detect any change of data in a determined object.
Until then, everything is ok. This block is invoked on all changes.
I have implemented a new process that uses a bunch of DispatchQueue and DispatchGroup, here is an example:
This code is just a sample! Don't do this!
DispatchQueue.global(qos: .background).async {
autoreleasepool {
//Other stuff...
let id = 1337
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
DispatchQueue(label: "Process").sync {
let foo = Bar()
foo.progress = { newValueInt in
let realm = try! Realm()
try! realm.write {
realm
.object(ofType: Some.self, forPrimaryKey: id)
.someValue = newValueInt
}
}
foo.completed = {
dispatchGroup.leave()
}
foo.doSomethingAsync()
}
dispatchGroup.notify(queue: DispatchQueue.global(qos: .background)) {
//Process completed.
}
}
}
The problem is: The addNotificationBlock is not invoked and the Object added on its notification block doesn't update when the method Bar.progress runs.
Thank you!
Here's a full app that uses your code, filling in the blanks that you didn't provide, and the notification block is correctly invoked:
import UIKit
import RealmSwift
class Bar {
var progress: ((Int) -> Void)?
var completed: (() -> Void)?
func doSomethingAsync() {
for delay in 1...100 {
// Span these 100 updates over 10 seconds
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + Double(delay) / 10) {
self.progress?(delay)
if delay == 100 {
self.completed?()
}
}
}
}
init() {}
}
class Some: Object {
dynamic var id = 0
dynamic var someValue = 0
override static func primaryKey() -> String? {
return "id"
}
}
func bgTask() {
DispatchQueue.global(qos: .background).async {
autoreleasepool {
//Other stuff...
let id = 1337
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
DispatchQueue(label: "Process").sync {
let foo = Bar()
foo.progress = { newValueInt in
let realm = try! Realm()
try! realm.write {
realm
.object(ofType: Some.self, forPrimaryKey: id)!
.someValue = newValueInt
}
}
foo.completed = {
dispatchGroup.leave()
}
foo.doSomethingAsync()
}
dispatchGroup.notify(queue: .global(qos: .background)) {
//Process completed.
}
}
}
}
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var notificationToken: NotificationToken!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
_ = try? FileManager.default.removeItem(at: Realm.Configuration.defaultConfiguration.fileURL!)
let realm = try! Realm()
let some = Some()
try! realm.write {
some.id = 1337
realm.add(some)
}
notificationToken = some.addNotificationBlock { change in
switch change {
case .change(let properties):
print(properties)
case .error(let error):
print("An error occurred: \(error)")
case .deleted:
print("The object was deleted.")
}
}
bgTask()
return true
}
}
This logs (truncated for brevity):
[RealmSwift.PropertyChange(name: "someValue", oldValue: Optional(0), newValue: Optional(1))]
[RealmSwift.PropertyChange(name: "someValue", oldValue: Optional(1), newValue: Optional(2))]
[RealmSwift.PropertyChange(name: "someValue", oldValue: Optional(2), newValue: Optional(3))]
[RealmSwift.PropertyChange(name: "someValue", oldValue: Optional(3), newValue: Optional(4))]
[RealmSwift.PropertyChange(name: "someValue", oldValue: Optional(4), newValue: Optional(5))]
...
[RealmSwift.PropertyChange(name: "someValue", oldValue: Optional(98), newValue: Optional(97))]
[RealmSwift.PropertyChange(name: "someValue", oldValue: Optional(97), newValue: Optional(100))]
The solution is very simple, you just need to run the update on the main thread.
foo.progress = { newValueInt in
DispatchQueue.main.sync {
let realm = try! Realm()
try! realm.write {
realm
.object(ofType: Some.self, forPrimaryKey: id)
.someValue = newValueInt
}
}
}

swift realm::IncorrectThreadException: Realm accessed from incorrect thread

I've created a model named "File", and it looks OK with Realm Browser:
but when I use the model, it will return error:
libc++abi.dylib: terminating with uncaught exception of type realm::IncorrectThreadException: Realm accessed from incorrect thread.
In my code, I create the Realm object every where I needs add/update:
private var allFiles : Results<File>!
private var downloadingFiles : Results<File>! {
return self.allFiles.filter("completed = false")
}
private var downloadedFiles : Results<File>! {
return self.allFiles.filter("completed = true")
}
private var downloading = false
private var request: Alamofire.Request?
func download() {
let fileRealm = try! Realm()
allFiles = fileRealm.objects(File).sorted("updatedAt")
downloadFile()
}
private func downloadFile() {
if !self.downloading, let file = self.downloadingFiles.first where !file.completed {
self.reqForDownload(file)
}
}
private func reqForDownload(file: File) -> Void {
downloading = true
request = Alamofire
.download(.GET, file.url, destination: { (url, response) -> NSURL in
return NSURL(fileURLWithPath: file.filePath)
})
.progress { [unowned self](bytesRead, totalBytesRead, totalBytesExpectedToRead) in
dispatch_async(dispatch_get_main_queue(), {
let variable = Float(totalBytesRead)/Float(totalBytesExpectedToRead)
debugPrint(variable)
})
}
.response { [unowned self](request, response, data, error) in
if let error = error {
dispatch_async(dispatch_get_main_queue(), {
let fileRealm = try! Realm()
try! fileRealm.write({
file.completed = false
})
self.allFiles = fileRealm.objects(File).sorted("updatedAt")
})
if error.code == NSURLErrorCancelled {
debugPrint("Canceled download")
}
} else {
debugPrint("Downloaded file successfully")
dispatch_async(dispatch_get_main_queue(), {
let fileRealm = try! Realm()
try! fileRealm.write({
file.completed = true
})
self.allFiles = fileRealm.objects(File).sorted("updatedAt")
})
}
self.downloading = false
}
}
I'm new for Realm but I know the Realm is not thread safe, so I'm tried to use the object in main thread as my code but the error still appeared. Please someone help me, thank you.
I've update my code as #TimOliver's suggest, but it still response the same error. New code as below:
private var allFiles : Results<File>!
private var downloadingFiles : Results<File>! {
return self.allFiles.filter("completed = false")
}
private var downloadedFiles : Results<File>! {
return self.allFiles.filter("completed = true")
}
private var downloading = false
private var request: Alamofire.Request?
func download() {
let fileRealm = try! Realm()
allFiles = fileRealm.objects(File).sorted("updatedAt")
downloadFile()
}
private func downloadFile() {
if !self.downloading, let file = self.downloadingFiles.first where !file.completed {
self.reqForDownload(file)
}
}
private func reqForDownload(file: File) -> Void {
downloading = true
request = Alamofire
.download(.GET, file.url, destination: { (url, response) -> NSURL in
return NSURL(fileURLWithPath: file.filePath)
})
.progress { [unowned self](bytesRead, totalBytesRead, totalBytesExpectedToRead) in
dispatch_async(dispatch_get_main_queue(), {
let variable = Float(totalBytesRead)/Float(totalBytesExpectedToRead)
debugPrint(variable)
})
}
.response { [unowned self](request, response, data, error) in
if let error = error {
let fileRealm = try! Realm()
try! fileRealm.write({
file.completed = false
})
self.allFiles = fileRealm.objects(File.self).sorted("updatedAt")
if error.code == NSURLErrorCancelled {
debugPrint("Canceled download")
}
} else {
debugPrint("Downloaded file successfully")
let fileRealm = try! Realm()
try! fileRealm.write({
file.completed = true
})
self.allFiles = fileRealm.objects(File.self).sorted("updatedAt")
}
self.downloading = false
}
}
Like I asked in the comments, if you set an exception breakpoint, you can see exactly which line of code is triggering the Realm exception so you can track in which thread the Realm transaction is occurring, as well as which objects are interacting with it.
If I recall correctly, I believe the closure called in the .response portion of that method doesn't get called on the main thread by default, however you're attempting to modify the file object that was definitely queried for on the main thread.
Short of forcing every closure in there to be called on the main thread, it would be more appropriate to have a primary key property in your file object, hold a reference directly to the primary key value, and then directly query for a thread local version of the file object when you need to update it (i.e. using the Realm.object(ofType: primaryKey:) method.
self.allFiles = fileRealm.objects(File.self).sorted("updatedAt") in the .response() closure was excuted sub thread. So you access self.allFiles on main thread later, it would crash.
Results instances are live, auto-updating views into the underlying data, which means results never have to be re-fetched. They always reflect the current state of the Realm on the current thread, including during write transactions on the current thread.
https://realm.io/docs/swift/latest/#auto-updating-results
So you do not need to re-fetch allFiles. A transaction was committed, allFiles are automatically up to date.

Realm is not updating existent objects

I'm using the Realm to save some objects and the user has the ability to reload them through a network function.
class Task: Object {
dynamic var id: String = ""
dynamic var price: Double = 0
override class func primaryKey() -> String {
return "id"
}
func fillNew(json: JSON) {
self.id = json["id"].stringValue
self.name = json["prive"].doubleValue
}
}
The network function get an array of json to build these objects. Once these new objects are created, I pass them through an add or update but when I fetch them again the fields have not changed.
func getPendingTasks(completion: (JSON?, NSError?) -> ()) {
let urlString = ".."
Alamofire.request(.GET, urlString).responseJSON { response in
let json = response.result.value != nil ? JSON(response.result.value!) : nil
dispatch_async(dispatch_get_main_queue()) {
completion(json, response.result.error)
}
}
}
func getTasks() {
self.getPendingTasks() { json, error in
if let error = error {
return
}
let jsonTasks = json!["tasks"].arrayValue
for jsonTask in jsonTasks {
let task = Task()
task.fillNew(jsonTask)
self.tasks.append(task);
}
self.tasks = Task.sortGenericTasks(self.tasks)
dispatch_async(dispatch_get_main_queue()) {
let realm = try! Realm()
try! realm.write {
realm.add(self.tasks, update: true)
}
}
}
}
I'm currently using the latest version of realm, but can not pinpoint exactly what is happening or if I'm doing something wrong.

Resources