Set Table Section Titles from Realm Query - ios

I've searched for the answer to this (I'm sure it's there somewhere) but can't find it.
I am trying to populate a UITableView's section headers from a Realm database, where the section title is in a related class.
My Realm classes:
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 = ""
override static func primaryKey() -> String? {
return "personId"
}
}
class Group: Object {
#objc dynamic var groupId = UUID().uuidString
#objc dynamic var person: Person?
#objc dynamic var groupName: String = ""
let groupContent = List<String>()
override static func primaryKey() -> String? {
return "groupId"
}
}
I want to retrieve the groupName results for the current user and use them as table section headers. The number of groupNames is dynamic for each user.
My current code, which doesn't work at all is:
func getGroupNames() {
let mobileNumber = UserDefaults.standard.integer(forKey: "mobileNumber")
let personResult = realm.objects(Person.self).filter("mobileNumber == %#", mobileNumber)
let groupNames = realm.objects(Group.self).filter("person == %#", personResult.self.first)
return (groupNames)
}
I can't get groupNames to be useful as section headers.
Help will be appreciated!
Thanks.
UPDATE
I now have:
func getGroupNames() -> [String] {
let mobileNumberInt = mobileNumber
let groupNames = realm.objects(Group.self).filter("person.mobileNumber == %#", mobileNumberInt).map({$0.groupName})
return Array(groupNames)
}
This returns ["Group Name 1", "Group Name 2"] twice (no matter how many objects are in the results). Why twice, and now how do I get these into my section headers? I've tried:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> [String] {
return getGroupNames()
}
The quantity of sections works, but the headers are not showing.

Assuming you already know the mobile number of a person, you can use following code to fetch groups a person belongs to.
func getGroupNames() -> [Group]? {
let realm = try Realm()
let mobileNumber = UserDefaults.standard.integer(forKey: "mobileNumber")
let groupNames = realm.objects(Group.self).filter("person.mobileNumber == %#", mobileNumber)
return Array(groupNames)
}

Related

Realm Update Deleting existing data

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
}

Adding an array of Json Data to Realm

I'm making an app for airports and I'm getting an array of data from one api, like so:
"data":[
{"id":"001","code":"ABZ","name":"Aberdeen","country":"United Kingdom"},
{"id":"002","code":"AUH","name":"Abu Dhabi","country":"United Arab Emirates"},
.
.
.
]
AND :
"airports":[
{"from":"001",
"to":["1","3","11","13","12","20","23","27","29","31","33"]
},
.
.
.
]
I have created realm model classes:
class AirportsDataRealm: Object {
#objc dynamic var name: String = ""
#objc dynamic var id: Int = 0
#objc dynamic var code: String = ""
#objc dynamic var country: String = ""
override static func primaryKey() -> String? {
return "id"
}
}
class AirportsFromToRealm: Object {
#objc dynamic var fromID: Int = 0
var toID = List<Int>()
override static func primaryKey() -> String? {
return "fromID"
}
}
now I want to save it into realm, I'm using swiftyJSON and I have used for-loop to do it and it is working fine but I think it's taking long time since the array is very long, here is what I've done:
// Airports Data
let countData = json["data"].count
for i in 0...countData - 1{
let airportsDataModel = AirportsDataRealm()
airportsDataModel.code = json["data"][i]["code"].stringValue
airportsDataModel.name = json["data"][i]["name"].stringValue
airportsDataModel.country = json["data"][i]["country"].stringValue
airportsDataModel.id = Int(json["data"][i]["id"].stringValue)!
try! realm.write {
realm.add(airportsDataModel, update: true)
}
}
//Airports FROM-TO
let countFromTo = json["airports"].count
for i in 0...countFromTo - 1{
let fromToDataModel = AirportsFromToRealm()
fromToDataModel.fromID = Int(json["airports"][i]["from"].stringValue)!
let arrayTo = json["airports"][i]["to"].arrayValue.map{ $0.intValue }
fromToDataModel.toID.append(objectsIn: arrayTo)
try! realm.write {
realm.add(fromToDataModel, update: true)
}
}
is there any way to save the whole array in realm in one shot without for-loop?
P.S
"there should be a relation between the two tables because each from 'id' has a list of 'to' id's and the id's are from the data table, for now I managed to create this relations when fetching the data using filters ,, so just ignore this"
Thank you
Simply use map method,
First I needed to add initializers to my object classes and pass json array as a parameter, like so:
class AirportsDataRealm: Object {
#objc dynamic var name: String = ""
#objc dynamic var id: Int = 0
#objc dynamic var code: String = ""
#objc dynamic var country: String = ""
convenience required init(withJSON json : JSON) {
self.init()
self.name = json["name"].stringValue
self.id = json["id"].intValue
self.code = json["code"].stringValue
self.country = json["country"].stringValue
}
override static func primaryKey() -> String? {
return "id"
}
}
class AirportsFromToRealm: Object {
#objc dynamic var fromID: Int = 0
var toID = List<Int>()
convenience required init(withJSON json : JSON) {
self.init()
self.fromID = json["from"].intValue
let toArray = json["to"].arrayValue.map{ $0.intValue }
self.toID.append(objectsIn: toArray)
}
override static func primaryKey() -> String? {
return "fromID"
}
}
Then by using map method the code will look like this:
func updateAirport(json: JSON) {
// Airports Data
let airportsData : [AirportsDataRealm]
let airportsDataJsonArray = json["data"].array
airportsData = airportsDataJsonArray!.map{AirportsDataRealm(withJSON: $0)}
//Airports FROM-TO
let airportsFromTo : [AirportsFromToRealm]
let airportsFromToJsonArray = json["airports"].array
airportsFromTo = airportsFromToJsonArray!.map{AirportsFromToRealm(withJSON: $0)}
//Write To Realm
try! realm.write {
realm.add(airportsData, update: true)
realm.add(airportsFromTo, update: true)
}
}
No for loops anymore ^_^

Realm add(_, update: true) removes existing relationships

I am facing an issue where I am unable to keep existing relationships after calling add(_, update: true) function.
I wrote a TaskSync class that is responsible for creating/updating Task objects:
class TaskSync: ISync {
typealias Model = Task
func sync(model: Task) {
let realm = try! Realm()
let inWrite = realm.isInWriteTransaction
if !inWrite {
realm.beginWrite()
}
let _task = realm.object(ofType: Task.self, forPrimaryKey: model.id)
// Persist matches as they are not getting fetched with the task
if let _task = _task {
print("matches: \(_task.matches.count)")
model.matches = _task.matches
}
realm.add(model, update: true)
if _task == nil {
var user = realm.object(ofType: User.self, forPrimaryKey: model.getUser().id)
if (user == nil) {
user = model.getUser()
realm.add(user!, update: true)
}
user!.tasks.append(model)
}
if !inWrite {
try! realm.commitWrite()
}
}
func sync(models: List<Task>) {
let realm = try! Realm()
try! realm.write {
models.forEach { task in
sync(model: task)
}
}
}
}
When a model is to be synced, I check if it already exists in the Realm and if so, I fetch it and try to include the matches property as this one is not included in the model.
Right before the call realm.add(model, update: true), model contains list of matches, however right after the realm.add is executed, the matches list is empty.
Here are the two models:
class Task: Object, ElementPreloadable, ElementImagePreloadable, ItemSectionable {
dynamic var id: Int = 0
dynamic var title: String = ""
dynamic var desc: String = ""
dynamic var price: Float = 0.0
dynamic var calculatedPrice: Float = 0.0
dynamic var location: String = ""
dynamic var duration: Int = 0
dynamic var date: String = ""
dynamic var category: Category?
dynamic var currency: Currency?
dynamic var longitude: Double = 0.0
dynamic var latitude: Double = 0.0
dynamic var state: Int = 0
dynamic var userId: Int = 0
// Existing images
var imagesExisting = List<URLImage>()
// New images
var imagesNew = List<Image>()
// Images deleted
var imagesDeleted = List<URLImage>()
private let users = LinkingObjects(fromType: User.self, property: "tasks")
var user: User?
var matches = List<Match>()
dynamic var notification: Notification?
override static func ignoredProperties() -> [String] {
return ["imagesExisting", "imagesNew", "imagesDeleted", "user", "tmpUser"]
}
override static func primaryKey() -> String? {
return "id"
}
func getImageMain() -> URLImage? {
for image in imagesExisting {
if image.main {
return image
}
}
return imagesExisting.first
}
func getSection() -> Int {
return state
}
func getSectionFieldName() -> String? {
return "state"
}
func getId() -> Int {
return id
}
func getURL() -> URL? {
if let image = getImageMain() {
return image.getResizedURL()
}
return nil
}
func getState() -> TaskOwnState {
return TaskOwnState(rawValue: state)!
}
func getUser() -> User {
return (user != nil ? user : users.first)!
}
}
class Match: Object, ElementPreloadable, ElementImagePreloadable, ItemSectionable {
dynamic var id: Int = 0
dynamic var state: Int = -1
dynamic var priorityOwnRaw: Int = 0
dynamic var priorityOtherRaw: Int = 0
dynamic var user: User!
var messages = List<Message>()
private let tasks = LinkingObjects(fromType: Task.self, property: "matches")
var task: Task?
dynamic var notification: Notification?
override static func primaryKey() -> String? {
return "id"
}
override static func ignoredProperties() -> [String] {
return ["task"]
}
func getId() -> Int {
return id
}
func getSection() -> Int {
return 0
}
func getURL() -> URL? {
if let image = user.getImageMain() {
return image.getResizedURL()
}
return nil
}
func getPriorityOwn() -> PriorityType {
if priorityOwnRaw == PriorityType.normal.rawValue {
return PriorityType.normal
}
else {
return PriorityType.favorite
}
}
func getPriorityOther() -> PriorityType {
if priorityOtherRaw == PriorityType.normal.rawValue {
return PriorityType.normal
}
else {
return PriorityType.favorite
}
}
func getSectionFieldName() -> String? {
return nil
}
func getTask() -> Task {
return (task != nil ? task : tasks.first)!
}
}
I spent hours trying to figure out why I am unable to keep the matches relationship when updating the task. Every advice will be highly appreciated!
This question was also asked upon Realm's GitHub issue tracker. For posterity, here is the solution.
List properties should always be declared as let properties, as assigning to them does not do anything useful. The correct way to copy all objects from one List to another is model.tasks.append(objectsIn: _user.tasks).

Search bar and sections in tableview using swift3

I have tableview with alphabetic sections from my database and I want to add search bar but i can't figure it out how to filter the data and implement it in the tableview.
My database store inside two structs:
one struct holding all the data.
second struct gets the first letter for the sections and the first struct as array.
My structs:
struct SentenceInfo { // First struct (holds all the data)
let name: String
let detail: String
let sentence: String
init(name: String, detail: String, sentence: String) {
self.name = name
self.detail = detail
self.sentence = sentence
}
}
struct SentenceNameSection { // Second struct (first letter and array of the first struct)
var firstLetter: String
var crimes: [SentenceInfo]
init(title: String, objects: [SentenceInfo]) {
firstLetter = title
crimes = objects
}
}
My tableView:
var sections : [SentenceNameSection]!
var crimeData = [SentenceNameSection]()
var filteredData = [SentenceNameSection]()
var shouldShowSearchResults = false
var searchController: UISearchController!
func updateSearchResults(for searchController: UISearchController) {
let searchString = searchController.searchBar.text
filteredData = crimeData.filter({ (crime) -> Bool in
let crimeMatch: String = crime // Error about types
return ((crimeMatch.range(of: searchString!) != nil))
})
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: sentenceTableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifer, for: indexPath) as! sentenceTableViewCell
let crime: SentenceInfo = sections[indexPath.section].crimes[indexPath.row]
cell.nameLabel.text = crime.name
cell.detailLabel.text = crime.detail
cell.sentenceLabel.text = crime.sentence
return cell
}
First of all crimeData contains SentenceNameSection which cannot be compared to String
Apart from that to filter the data source array including sections you have to use a repeat loop and create new SentenceNameSection items
This code searches for all three properties in the SentenceInfo struct
let searchString = searchController.searchBar.text!
filteredData.removeAll() // is mandatory to empty the filtered array
for section in crimeData {
let filteredContent = section.crimes.filter { $0.name.range(of: searchString) != nil
|| $0.detail.range(of: searchString) != nil
|| $0.sentence.range(of: searchString) != nil
}
if !filteredContent.isEmpty {
filteredData.append(SentenceNameSection(title: section.firstLetter, objects: filteredContent))
}
}
Note: Of course you have to handle the search case in all appropriate table view data source and delegate methods.
For Swift 3 , below is the sample code
Struct BookDetails{
var title:String?
var author:String?
}
var filteredSearch:[BookDetails] = []
filteredSearch = self.bookDetails.filter { (data) -> Bool in
return data.title?.range(of: searchText, options: String.CompareOptions.caseInsensitive) != nil || data.author?.range(of: searchText, options: String.CompareOptions.caseInsensitive) != nil
}

iOS Realm Filter objects in a list of a relationship

I have three objects nested via lists like this:
class Canteen: Object {
dynamic var name: String?
let lines = List<Line>()
}
class Line: Object {
dynamic var name: String?
let meals = List<Meal>()
}
class Meal: Object {
dynamic var name: String?
dynamic var vegan: Bool = false
}
Getting all canteens with all the lines and meals is no problem. What im doing right now is this:
let predicate = NSPredicate(format: "name == %#", selectedCanteenType.rawValue)
canteens = realm.objects(Canteen).filter(predicate)
But now i only need the meals which are vegan. So im looking to get the selected canteen with all the lines, but only with meals which are vegan. Is this possible in realm, to filter lists in retrieved objects?
Realm doesn't have any sort of concept of a deep-filtered view, so you can't have a Results<Canteen> which restricts the Lists contained in related objects to vegan meals.
There are several similar things which you can do. You could add inverse relationship properties, and then query Meal objects instead:
class Canteen: Object {
dynamic var name: String?
let lines = List<Line>()
}
class Line: Object {
dynamic var name: String?
let meals = List<Meal>()
let canteens = LinkingObjects(fromType: Canteen.self, property: "lines")
}
class Meal: Object {
dynamic var name: String?
dynamic var vegan: Bool = false
let lines = LinkingObjects(fromType: Line.self, property: "meals")
}
let meals = realm.objects(Meal).filter("vegan = true AND ANY lines.canteens.name = %#", selectedCanteenType.rawValue)
(Or rather, you will be able to once Realm 0.102.1 is out; currently this crashes).
If you just need to iterate over the meals but need to do so from the Canteen down, you could do:
let canteens = realm.objects(Canteen).filter("name = %# AND ANY lines.meals.vegan = true", selectedCanteenType.rawValue)
for canteen in canteens {
for line in canteen.lines.filter("ANY meals.vegan = true") {
for meal in line.meals.filter("vegan = true") {
// do something with your vegan meal
}
}
}
This unfortunately has some duplication due to needing to repeat the filter for each level of the references.
Try this:
let predicate = NSPredicate(format: "name == %#", "")
var canteens: [Canteen] = realm.objects(Canteen).filter(predicate).map { can in
// Iterate through all the Canteens
let lines: [Line] = can.lines.map { (line: Line) in
// Iterate through all the lines in each canteene
// Filter all the Meals that are NOT vegan
let meals = line.meals.filter { $0.vegan == true }
line.meals = List<Meal>(meals)
return line
}
can.lines = List<Line>(lines)
return can
}
Realm allows it to use functions as parameter for the filtering. So this is my solution which im currently using.
The two filter functions:
func vegetarianFilter(_ meal: Meal) -> Bool {
if showVegetarianOnly {
if(meal.veg || meal.vegan){
return true
}
return false
}
return true
}
func filterEmptyLines(_ line: Line) -> Bool {
if(line.meals.filter(vegetarianFilter).count > 0){
return true
}
return false
}
The functions filter all meals which are not vegetarian or vegan when the user has selected showVegetarianOnly = true. Also it filters all lines which than have no meal left (nothing is vegetarian or vegan).
Most important functions of the TableView:
override func numberOfSections(in tableView: UITableView) -> Int {
return canteenDay?.lines.filter(filterEmptyLines).count ?? 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return canteenDay?.lines.filter(filterEmptyLines)[section].meals.filter(vegetarianFilter).count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let meal = canteenDay!.lines.filter(filterEmptyLines)[indexPath.section].meals.filter(vegetarianFilter)[indexPath.row]
cell.textLabel?.text = meal.meal
return cell
}

Resources