I have a case where I need to populate realm with a series of objects upon user request. To populate realm I make a network call then write the objects to realm. When the write is complete, the background thread invokes a callback that switches to the main thread to process the results. I would much prefer to use realm notifications but this is a very specific use case and notifications are not an option.
I can't share my full project but I was able to reproduce this issue with a sample project. Here is my data model.
class Owner: Object {
#objc dynamic var uuid: String = ""
#objc dynamic var name: String = ""
convenience init(uuid: String, name: String) {
self.init()
self.uuid = uuid
self.name = name
}
func fetchDogs() -> Results<Dog>? {
return realm?.objects(Dog.self).filter("ownerID == %#", uuid)
}
override class func primaryKey() -> String? {
return "uuid"
}
}
class Dog: Object {
#objc dynamic var uuid: String = ""
#objc dynamic var name: String = ""
#objc dynamic var ownerID: String = ""
func fetchOwner() -> Owner? {
return realm?.object(ofType: Owner.self, forPrimaryKey: ownerID)
}
convenience init(uuid: String, name: String, ownerID: String) {
self.init()
self.uuid = uuid
self.name = name
self.ownerID = ownerID
}
override class func primaryKey() -> String? {
return "uuid"
}
}
I realize that a List would be appropriate for the Owner -> Dog relationship but that's not an option in this project.
In the sample project there are two buttons that we care about. One to add some Dog objects to realm and associate them with the Owner UUID. The other button deletes all of the Dog objects that belong to the owner.
The basic flow is like this:
Delete all of the owner's dogs
Create a new set of dogs on a background thread and add them to realm.
Invoke a callback
Switch back to the main thread
Print all of the owner's dogs
Nine times out of ten the dogs are printed out successfully. However, sometimes realm indicates that there are no dogs associated with the current owner. What could be the reason for this? My view controller logic is below.
EDIT: The last thing I should mention is that this issue is only only reproducible on a physical device. I can't reproduce it on the simulator. That makes me wonder if this is a realm bug.
// MARK: - Props and view controller life cycle
class ViewController: UIViewController {
var owner: Owner?
let ownerUUID = UUID().uuidString
override func viewDidLoad() {
super.viewDidLoad()
owner = makeOwner()
}
}
// MARK: - Action methods
extension ViewController {
#IBAction func onDeletePressed(_ sender: UIButton) {
print("Deleting...")
deleteDogs()
}
#IBAction func onRefreshPressed(_ sender: UIButton) {
print("Creating...")
createDogs {
DispatchQueue.main.async {
self.printDogs()
}
}
}
#IBAction func onPrintPressed(_ sender: UIButton) {
printDogs()
}
}
// MARK: - Helper methods
extension ViewController {
private func makeOwner() -> Owner? {
let realm = try! Realm()
let owner = Owner(uuid: ownerUUID, name: "Bob")
try! realm.write {
realm.add(owner)
}
return owner
}
private func deleteDogs() {
guard let dogs = owner?.fetchDogs() else { return }
let realm = try! Realm()
try! realm.write {
realm.delete(dogs)
}
}
private func createDogs(completion: #escaping () -> Void) {
DispatchQueue(label: "create.dogs.background").async {
autoreleasepool {
let realm = try! Realm()
let dogs = [
Dog(uuid: UUID().uuidString, name: "Fido", ownerID: self.ownerUUID),
Dog(uuid: UUID().uuidString, name: "Billy", ownerID: self.ownerUUID),
Dog(uuid: UUID().uuidString, name: "Sally", ownerID: self.ownerUUID),
]
try! realm.write {
realm.add(dogs)
}
}
completion()
}
}
private func printDogs() {
guard let dogs = owner?.fetchDogs() else { return }
print("Dogs: \(dogs)")
}
}
It appears the the dogs are created on a separate thread; a 'background thread' and because of that it won't have a run loop - so the two threads are looking at different states of the data.
When creating objects on a thread with no runloop, Realm.refresh() must be called manually in order to advance the transaction to the most recent state.
Check out the documentation on Seeing changes from other threads
Related
Using Swift 5 for iOS13.
I am trying to update an existing Realm record with a Contact Picker result. The function deletes all the object content except for the new content.
My code
class Person: Object {
#objc dynamic var personId = UUID().uuidString
#objc dynamic var firstName: String = ""
#objc dynamic var surname: String = ""
#objc dynamic var mobileNumber: Int = 0
#objc dynamic var password: String = ""
#objc dynamic var myContactID: String = ""
override static func primaryKey() -> String? {
return "personId"
}
}
extension HomeController: CNContactPickerDelegate {
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
picker.dismiss(animated: true, completion: nil)
let person = Person()
let me = realm.objects(Person.self).filter("mobileNumber == %#", mobileNumber)
person.myContactID = contact.identifier
person.personId = me.first!.personId
try! realm.write {
realm.add(person, update: .modified)
}
self.viewWillAppear(true)
}
}
All the existing content of the Person class in the Realm database disappears except for myContactID and personID.
That is because you are updating the Person with new data.
The line let person = Person() creates a new instance with all the default values. (firstName: String = "" etc..)
So, when you assign myContactID and personId to this newly created person, it will look like this:
Person {
personId = FAE4C224-D37E-4C77-B6F1-C60A92F188D0;
firstName = ;
surname = ;
mobileNumber = ;
password = ;
myContactID = contactIdentifier;
}
And when you call realm.add(person, update: .modified), it will overwrite the record associated with the primary key with this newly created person.
You want to fetch an existing person and modify it. You can do something like this:
guard let me = realm.objects(Person.self).filter("mobileNumber == %#", mobileNumber).first else { return }
try! realm.write {
me.myContactID = contact.identifier
}
I am new to MVC design pattern. I created "DataModel" it will make an API call, create data, and return data to the ViewController using Delegation and "DataModelItem" that will hold all data. How to call a DataModel init function in "requestData" function. Here is my code:
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[DataModelItem])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
func requestData() {
}
private func setDataWithResponse(response:[AnyObject]){
var data = [DataModelItem]()
for item in response{
if let tableViewModel = DataModelItem(data: item as? [String : String]){
data.append(tableViewModel)
}
}
delegate?.didRecieveDataUpdata(data: data)
}
}
And for DataModelItem:
class DataModelItem{
var name:String?
var id:String?
init?(data:[String:String]?) {
if let data = data, let serviceName = data["name"] , let serviceId = data["id"] {
self.name = serviceName
self.id = serviceId
}
else{
return nil
}
}
}
Controller:
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
How to implement simple MVC design pattern in Swift?
As a generic answer, in iOS development you're already doing this implicitly! Dealing with storyboard(s) implies the view layer and controlling the logic of how they work and how they are connected to the model is done by creating view controller, that's the default flow.
For your case, let's clarify a point which is: according to the standard MVC, by default the responsible layer for calling an api should be -logically- the view controller. However for the purpose of modularity, reusability and avoiding to create massive view controllers we can follow the approach that you are imitate, that doesn't mean that its the model responsibility, we can consider it a secondary helper layer (MVC-N for instance), which means (based on your code) is DataModel is not a model, its a "networking" layer and DataModelItem is the actual model.
How to call a DataModel init function in "requestData" function
It seems to me that it doesn't make scene. What do you need instead is an instance from DataModel therefore you could call the desired method.
In the view controller:
let object = DataModel()
object.delegate = self // if you want to handle it in the view controller itself
object.requestData()
I am just sharing my answer here and I am using a codable. It will be useful for anyone:
Model:
import Foundation
struct DataModelItem: Codable{
struct Result : Codable {
let icon : String?
let name : String?
let rating : Float?
let userRatingsTotal : Int?
let vicinity : String?
enum CodingKeys: String, CodingKey {
case icon = "icon"
case name = "name"
case rating = "rating"
case userRatingsTotal = "user_ratings_total"
case vicinity = "vicinity"
}
}
let results : [Result]?
}
NetWork Layer :
import UIKit
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[String])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
var theatreNameArray = [String]()
var theatreVicinityArray = [String]()
var theatreiconArray = [String]()
func requestData() {
Service.sharedInstance.getClassList { (response, error) in
if error != nil {
self.delegate?.didFailUpdateWithError(error: error!)
} else if let response = response{
self.setDataWithResponse(response: response as [DataModelItem])
}
}
}
private func setDataWithResponse(response:[DataModelItem]){
for i in response[0].results!{
self.theatreNameArray.append(i.name!)
self.theatreVicinityArray.append(i.vicinity!)
self.theatreiconArray.append(i.icon!)
}
delegate?.didRecieveDataUpdata(data: theatreNameArray)
print("TheatreName------------------------->\(self.theatreNameArray)")
print("TheatreVicinity------------------------->\(self.theatreVicinityArray)")
print("Theatreicon------------------------->\(self.theatreiconArray)")
}
}
Controller :
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
APIManager :
class Service : NSObject{
static let sharedInstance = Service()
func getClassList(completion: (([DataModelItem]?, NSError?) -> Void)?) {
guard let gitUrl = URL(string: "") else { return }
URLSession.shared.dataTask(with: gitUrl) { (data, response
, error) in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(DataModelItem.self, from: data)
completion!([gitData],nil)
} catch let err {
print("Err", err)
completion!(nil,err as NSError)
}
}.resume()
}
}
I would recommend using a singleton instance for DataModel, since this would be a class you would be invoking from many points in your application.
You may refer its documentation at :
Managing Shared resources using singleton
With this you wont need to initialise this class instance every time you need to access data.
I am using realm to store and persist my data. Everything works fine and I just discovered that users can actually store duplicate item which is bad. I would am unable to create a check to prevent duplicate items, any help would be appreciated
Function
func addData(object: OfflineModel) {
try! database.write {
database.add(object, update: true)
}
}
//MARK:- Get Offline
func getDataFromDB() -> Results<OfflineModel> {
offlineItems = database.objects(OfflineModel.self)
return offlineItems!
}
//MARK:- Create Offline
func createOfflineList(createdDate: Date, photo: Data, title: String, completion: #escaping CompletionHandler) -> Void {
REALM_QUEUE.sync {
let offlineList = OfflineModel()
offlineList.createdDate = createdDate
offlineList.photo = photo
offlineList.title = title
OfflineFunctions.instance.addData(object: offlineList)
completion(true, nil)
}
}
Model
#objc dynamic var id = UUID().uuidString
#objc dynamic var photo: Data? = nil
#objc dynamic var title : String = ""
#objc dynamic var createdDate: Date?
override static func primaryKey() -> String {
return "id"
}
The issue is that in the createOfflineList method you create a new OfflineModel, which generates a random id using UUID().uuidString and hence you cannot have duplicate models from Realm's point of view, since id, which is used as the primary key will always be different. You'll need to use title (or any other non-random property that you actually want to use to identify your model instances) as the primary key.
class OfflineModel: Object {
#objc dynamic var photo: Data? = nil
#objc dynamic var title : String = ""
#objc dynamic var createdDate: Date?
override static func primaryKey() -> String {
return "title"
}
}
I am a junior iOS developer. I want to refactor my code. currently I want to refactor my realm methods. to save or delete data in Realm, I need to use a lot of do try catch block all over the place, so I want to make a service to handle this. I have tried but I don't know if this is the correct way or not
import RealmSwift
class RealmService {
private init() {}
static let shared = RealmService()
var realm = try! Realm()
func save(expression: ()) {
do {
try realm.write {
// I want to pass the expression in this block here
expression
}
} catch {
post(error)
}
}
}
usage:
func saveToRealmDatabase(wishList: WishList, product: Product) {
RealmService.shared.save(expression: {
// I want to pass expression in the block in here as argument
wishList.products.append(product)
}())
}
I want to pass the expression wishList.products.append(product) as argument of the function in RealmService class. is there a better way to achieve that ?
Your "expression" can be represented by the type () -> Void - a closure that accepts no parameters and returns nothing.
This is how your method should be declared:
func save(expression: #escaping () -> Void) {
do {
try realm.write {
expression()
}
} catch {
post(error)
}
}
This is how you should use it:
func saveToRealmDatabase(wishList: WishList, product: Product) {
RealmService.shared.save(expression: {
wishList.products.append(product)
})
}
From the approach you are trying i can guess you want to do something like,
RealmService.shared.save(expression: {
wishList.products.append(product)
realm.add(wishList.products)
})
So based on the understanding that you want to perform the addition operation in that callback, i would rather suggest the below implementation where you don't need to implement the callback for every operation. Rather when you are interested to know the status of the operation then only you would implement the callback. Check the below snippet,
class RealmService {
private init() {}
static let shared = RealmService()
var realm = try! Realm()
/// Saving a list of object i.e, List<Object>
func save<O, L: List<O>>(_ list: L, callback: ((Error?) -> Void)? = nil) where O: Object {
do {
try realm.write {
realm.add(list)
callback?(nil)
}
} catch {
callback?(error)
}
}
/// Saving a single object i.e, `Object`
func save(_ object: Object, callback: ((Error?) -> Void)? = nil) {
do {
try realm.write {
realm.add(object)
callback?(nil)
}
} catch {
callback?(error)
}
}
}
Usage
class Dog: Object {
#objc dynamic var name = ""
#objc dynamic var age = 0
}
let tomy = Dog()
tomy.name = "Tomy"
let johny = Dog()
johny.name = "Johny"
let dogs = List<Dog>()
dogs.append(tomy)
dogs.append(johny)
//When you are not interested in save operation status
RealmService.shared.save(dogs)
//When you want to handle error while saving
RealmService.shared.save(dogs) { [weak self] in self?.showAlert($0) }
func showAlert(_ error: Error?) {
guard let e = error else { return }
showAlert(alertTitle: "Error", alertMessage: e.localizedDescription, actionTitle: "Back")
}
I want to refactor my code to apply clean approach.
I have a class user
class User {
let name: String?
let id: String
var isOnline: Bool
var mesaageHistory = [Message]()
init(name: String, id: String, isOnlineStatus: Bool) {
self.name = name
self.id = id
self.isOnline = isOnlineStatus
}
}
Then I'm using fabric pattern to create list of my users.
protocol CreateUserProtocol: class {
func sortArray(inputArr: [User])
}
class CreateUserEntity: CreateUserProtocol {
static let shared = CreateUserEntity()
var users = [User]()
func sortArray(inputArr: [User]){
var applySortingByDate: Bool = false
for user in inputArr {
if !user.mesaageHistory.isEmpty {
applySortingByDate = true
}
}
if applySortingByDate {
inputArr.sorted(by: { (first, second) -> Bool in
(first.mesaageHistory.last?.messageTime)! < (second.mesaageHistory.last?.messageTime)!
})
} else {
inputArr.sorted(by: { (first, second) -> Bool in
first.name! < second.name!
})
}
}
}
One controller is responsible for appending new users, while another is used to retrieve them and bind them to tableView. Everything is working fine, but I think my solution is not good enough for scaling.
Moreover in one of my VC I use to sort my Users to online and offline. I think, that I shouldn't do that in my VC and to put this logic into my CreateUserEntity
var onlineUsersData = [User]()
var offlineUsersData = [User]()
private func classifyUsers() {
for user in CreateUserEntity.shared.users {
print("is user online: \(user.isOnline)")
print(CreateUserEntity.shared.users.count)
if user.isOnline == true && !onlineUsersData.contains(user) {
onlineUsersData.append(user)
}
if user.isOnline == false && !offlineUsersData.contains(user) {
offlineUsersData.append(user)
}
}
}
I'd like to rewrite it in proper way, what can you recommend me?
From my opinion try to use firstly struct instead of the class.
Example:
struct User {
let name: String
}
Then you should figure out where you will be storing these users? Now they are in the memory. So we should define where and how we will be storing them.
So probably for this case we can consider NSUserDefaults as core for the class that will store users.After that we should create facade to this class to manage our users.
protocol UserStoreFacade {
func fetch(name withName:String) -> User
func create(name withName:String) -> User
func save(user:User)
func update(name newName:String) -> User
func delete(name withName:String)
}
And UserStore that is used to manage user.
class UserStore: UserStoreFacade {
let defaults = UserDefaults(suiteName: "User")
func fetch(name withName:String) -> User {
let encodeData = defaults?.dictionary(forKey: withName)
return User(dictionary: encodeData as! Dictionary<String, AnyObject>)
}
func create(name withName: String) -> User {
return User(name: withName)
}
func save(user: User) {
defaults?.set(user.encode(), forKey: user.name)
}
func update(name newName:String) -> User {
return User(name: newName)
}
func delete(name withName:String) {
defaults?.removeObject(forKey: withName)
}
}
It is quite primitive but still showing case how it is possible to accomplish.