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
}
}
}
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.
I have a task aggregator which aggregates and executes tasks in a synchronized manner. It's thread safe and all that. Here's what it looks like
class DaddysMagicalTaskAggregator {
private let tasks: ThreadSafeValueContainer<[KewlTask]>
private let cancelled: ThreadSafeValueContainer<Bool>
private var completion: ((Result<Bool, Error>) -> Void)?
private var alreadyCompleted: Result<Bool, Error>?
private var getTasks: [KewlTask] {
return tasks.value ?? []
}
private var isCancelled: Bool {
return cancelled.value ?? true
}
init(queue: DispatchQueue = DispatchQueue(label: "DaddysMagicalTaskAggregator")) {
self.tasks = ThreadSafeValueContainer(value: [], queue: queue)
self.cancelled = ThreadSafeValueContainer(value: false, queue: queue)
}
/// Add a task to the list of tasks
func addTask(_ task: KewlTask) -> DaddysMagicalTaskAggregator {
self.tasks.value = getTasks + [task]
return self
}
/// Add tasks to the list of tasks
func addTasks(_ tasks: [KewlTask]) -> DaddysMagicalTaskAggregator {
self.tasks.value = getTasks + tasks
return self
}
/// Start executing tasks
#discardableResult
func run() -> DaddysMagicalTaskAggregator {
guard !isCancelled else {
return self
}
guard !getTasks.isEmpty else {
alreadyCompleted = .success(true)
completion?(.success(true))
return self
}
var currentTasks = getTasks
let taskToExecute = currentTasks.removeFirst()
self.tasks.value = currentTasks
taskToExecute.execute { (result) in
switch result {
case .success:
self.run()
case.failure(let error):
self.taskFailed(with: error)
}
}
return self
}
private func taskFailed(with error: Error) {
tasks.value = []
alreadyCompleted = .failure(error)
completion?(.failure(error))
completion = nil
}
/// Add a completion block which executes after all the tasks have executed or upon failing a task.
func onCompletion(_ completion: #escaping (Result<Bool, Error>) -> Void) {
if let result = alreadyCompleted {
completion(result)
} else {
self.completion = completion
}
}
/// Cancel all tasks
func cancelAllTasks(with error: Error) {
cancelled.value = true
taskFailed(with: error)
}
}
public class KewlTask {
private let closure: ((KewlTask) -> Void)
private var completion: ((Result<Bool, Error>) -> Void)?
public init(_ closure: #escaping (KewlTask) -> Void) {
self.closure = closure
}
public func execute(_ completion: #escaping (Result<Bool, Error>) -> Void) {
self.completion = completion
closure(self)
}
/// Task succeeded
func succeeded() {
completion?(.success(true))
}
/// Task failed with given error
func failed(with error: Error) {
completion?(.failure(error))
}
/// Take action based on the result received
func processResult(_ result: Result<Bool, Error>) {
switch result {
case .success:
succeeded()
case .failure(let error):
failed(with: error)
}
}
}
public class ThreadSafeContainer {
fileprivate let queue: DispatchQueue
public init(queue: DispatchQueue) {
self.queue = queue
}
}
public class ThreadSafeValueContainer<T>: ThreadSafeContainer {
private var _value: T
public init(value: T, queue: DispatchQueue) {
self._value = value
super.init(queue: queue)
}
public var value: T? {
get {
return queue.sync { [weak self] in
self?._value
}
}
set(newValue) {
queue.sync { [weak self] in
guard let newValue = newValue else { return }
self?._value = newValue
}
}
}
}
It works as expected. However, when I write a test to make sure it deallocates, the test keeps failing even after the expectation is fulfilled.
Please look at the test code below
import XCTest
class AggregatorTests: XCTestCase {
func testTaskAggregatorShouldDeallocatateUponSuccess() {
class TaskContainer {
let aggregator: CustomKewlAggregator
init(aggregator: CustomKewlAggregator = CustomKewlAggregator()) {
self.aggregator = aggregator
}
}
class CustomKewlAggregator: DaddysMagicalTaskAggregator {
var willDeinit: (() -> Void)?
deinit {
willDeinit?()
}
}
let myExpecation = self.expectation(description: "Task Aggregator should deallocate")
var container: TaskContainer? = TaskContainer()
let t1 = KewlTask { t in
t.succeeded()
}
let t2 = KewlTask {
$0.succeeded()
}
container?.aggregator.willDeinit = {
myExpecation.fulfill()
}
container?.aggregator
.addTasks([t1, t2])
.run()
.onCompletion { (result) in
container = nil
}
waitForExpectations(timeout: 4, handler: nil)
}
}
I've added breakpoints and everything to ensure the expectation fulfillment code does execute.
It doesn't seem to be an xCode issue since I've tested it on XCode 11.7, 12.1, 12.2, and 12.4.
Any idea what's going on here? To me, it looks like a bug in XCTests.
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)
}
Here is my ViewModel Call with function fetch Products, I need to test function which is internally calling Async request and setting some data
class ViewModel : NSObject {
public var array : [Product]?
func fetchProduct() {
ProductRouter.fetchByCategory.send(modelType: ProductSearchResponse.self, success: { (success) in
self.array = (success as! ProductSearchResponse).skus
}, fail: { (error : NSError) in
print(error.localizedDescription)
}, showHUD: true)
}
}
class MyNetworkRequestTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
let url = Bundle(for: type(of: self)).url(forResource: "Listing", withExtension: "json")!
let data = try! Data(contentsOf: url)
stub(uri(ProductRouter.fetchByCategory.path), jsonData(data))
let vm = ViewModel()
vm.fetchProduct()
XCTAssertNotNil(vm.sku)
}
}
// Json File have some Listing.json have correct json format.
So what you want to do is set up an expectation and wait for it
class MyNetworkRequestTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
let expectation = expectation(description: "fetch expectation")
let url = Bundle(for: type(of: self)).url(forResource: "Listing", withExtension: "json")!
let data = try! Data(contentsOf: url)
stub(uri(ProductRouter.fetchByCategory.path), jsonData(data))
let vm = ViewModel()
vm.fetchDone = {
expectation.fullfill()
}
vm.fetchProduct()
waitForExpectations(timeout: 10) { (error) in
XCTAssertNotNil(vm.sku)
}
}
}
The "fetchDone" is this case will be whatever tells your viewController that the data has arrived. Basically looking like this:
class vc: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = vm()
viewModel.fetchDone = {
self.populate(array: viewModel.array)
}
}
func populate(array: [String]) {
}
}
class vm: NSObject {
var fetchDone: (() ->())?
var array: [String] = [] {
didSet {
fetchDone?()
}
}
func fetchProduct() {
// Do something
self.array = ["some data"]
}
}
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!