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.
Related
I can see the exc bad excess on crashylitics , following is the code base . Is there anything wrong I am doing ? Crash is occurring on following line
guard let data = userData, let userDetails = try? JSONDecoder().decode(UserDetailsThreadSafe.self, from: data) else {
class UserDetailsThreadSafe:UserDetails
{
let queue = DispatchQueue(label: "auth-thread-safe-queue", attributes: .concurrent)
static let shared = UserDetailsThreadSafe()
private override init()
{
super.init()
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
fileprivate func getUserDetails() -> UserDetailsThreadSafe? {
queue.sync {
// perform read and assign value
var userData: Data? = LoginAuth.sharedInstance().defaultStorage?.secureValue(forKey: kUserDetails)
if userData == nil {
userData = UserDefaults.standard.value(forKey: kUserDetails) as? Data
}
guard let data = userData, let userDetails = try? JSONDecoder().decode(UserDetailsThreadSafe.self, from: data) else {
return nil
}
return userDetails
}
}
fileprivate func archieveData() {
queue.async(flags: .barrier) {
// perform writes on data
if let data = try? JSONEncoder().encode(self), data.isEmpty == false, let jsonString = String(data: data, encoding: .utf8) {
LoginAuth.sharedInstance().defaultStorage?.setSecure(jsonString, forKey: kUserDetails)
}
}
}
}
When adding tasks to your queue it is recommended to use "async" with the Barrier flag:
queue.async(flags: .barrier) {...}
Since we use async and not sync, the signature of getUserDetails() should change. This function should now pass back the result values using completion block, and return void.
UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION
I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.
If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is
and/or refactoring for more efficiency / readability!
Proposed Solution with GCD and updated example
This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
print("Error in setting up snapshot listener - \(error)")
} else {
let fireStoreDispatchGrp = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
//NEW EXAMPLE WITH ADDITIONAL TASK HERE
if let author = $0.data()["author"] as? DocumentReference {
author.getDocument() {(authorSnapshot, error) in
fireStoreDispatchGrp.enter() //1
if let error = error {
print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
fireStoreDispatchGrp.leave()
return
}
if let newAuthor = authorSnapshot.flatMap(Author.init) {
print("Able to build new author \(newAuthor)")
dataDict["author"] = newAuthor
dataDict["authorId"] = authorSnapshot?.documentID
print("Data Dict successfully mutated \(dataDict)")
}
fireStoreDispatchGrp.leave() //2
}
}
///END OF NEW EXAMPLE
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
fireStoreDispatchGrp.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
dataDict["image"] = url.absoluteString
case .failure(let error):
print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
returnArray.append(newArticle)
}
fireStoreDispatchGrp.leave() ///3
}
}
}
//Completion block
print("Exiting dispatchGroup all data should be setup correctly")
fireStoreDispatchGrp.notify(queue: .main) { ///4
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
Original Code
Calling Setup Code
self.manager.SetupArticleListener() { [weak self] articles in
print("πππππππIn closure function to update articlesπππππππ")
self?.articles = articles
}
Article Listener
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
}
}
}
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
updateListeners(for: listener)
}
GetURL
private func getURL(reference: StorageReference, _ result: #escaping (Result<URL, Error>) -> Void) {
reference.downloadURL() { (url, error) in
if let url = url {
result(.success(url))
} else {
if let error = error {
print("error")
result(.failure(error))
}
}
}
}
You need dispatch group as the for loop contains multiple asynchronous calls
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
let g = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
g.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
g.leave() /// 3
}
}
}
g.notify(queue:.main) { /// 4
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
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!
I'm trying to get some data from the server and use it globally in the app..
I mean for example, I'm using following code to get data from service:
struct Service : Decodable{
let id: Int
let name, description: String
let createdAt: String?
let updatedAt: String?
}
func makeGetCall() {
let todoEndpoint: String = "http://web.src01.view.beta.is.sa/public/api/services"
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling GET on /public/api/services")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let todos = try decoder.decode([Service].self, from: responseData)
for todo in todos{
print(todo.name)
}
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
This code is located and called in HomeViewController and i'm getting data which i want.
But i want to access and use this data in another viewcontroller and in whole app...
How i can do it? How can i make the received data from the function is saved globally and how to use it in another viewcontroller?
Can someone tell me how i can do this?
For such cases we usually use static data. They may be served as singleton or just a static property. In your case a static property for cached data may be nice. We can put static properties in extension so adding following may be nice:
// MARK: - Fetching Data
extension Service {
private static var cachedServices: [Service]?
static func fetchServices(_ completion: (_ services: [Service]) -> Void?) {
if let cachedServices = cachedServices {
completion(cachedServices)
} else {
makeGetCall { services in
let newServices = services ?? []
self.cachedServices = newServices
completion(newServices)
}
}
}
}
Now the usage from everywhere is calling
Service.fetchServices { services in
}
and this call may be asynchronous or not, depending if data is already loaded.
If you need to access them synchronous and you are sure data is already loaded then simply add another method in extension:
static func getCachedData() -> [Service] {
return cachedServices ?? []
}
This method will return instantly but array will be empty if no data was received yet. But anywhere you can call Service.getCachedData()
This cache is now only preserved until your app terminates. If you want to preserve them longer then all you need to do is add the logic to save and load data into file or user defaults. The logic for that would be something like:
private static var cachedServices: [Service]? {
didSet {
self.saveServicesToFile(cachedServices)
}
}
static func fetchServices(_ completion: (_ services: [Service]) -> Void?)
{
if let cachedServices = cachedServices {
completion(cachedServices)
} else if let saved = self.loadFromFile() {
self.cachedServices = saved
completion(saved)
}else {
makeGetCall { services in
let newServices = services ?? []
self.cachedServices = newServices
completion(newServices)
}
}
}
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.