iOS Swift Couchbase Lite Primary Key and Class Configuration - ios

I was going through couchbase-lite to use it in my next iOS app. I have created a model named Surah for now. Definitely, I will have more model classes later.
Basically I have four questions here.
How do I add _id as my primary key in couchbase-lite?
As I will be having more classes how will I handle those? As I am creating
MutableDocument, How will that differentiate each my classes?
As I can see I have to iterate through each of my items to batch insert, won't that become slow for the large datasets?
How do i convert results from a query with large data to a array of Model Class. (in this case of array of Surah)
class Surah: Decodable {
enum Keys: String, CodingKey {
case _id
case index
case englishName
case englishMeaning
case name
case place
case count
}
var _id = ""
var index = 1
var page = 1
var numberOfAyahs = 1
var englishName = ""
var englishMeaning = ""
var name = ""
var place = ""
var isFavorite = false
var dictionary: [String: Any] {
return ["_id": _id, "index": index, "page": page]
}
required init() {}
required init(_id: String, index: Int, name: String, englishName: String, englishMeaning: String, place: String, count: Int) {
self._id = _id
self.index = index
self.name = name
self.englishName = englishName
self.englishMeaning = englishMeaning
self.place = place
self.numberOfAyahs = count
}
required convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self) // defining our (keyed) container
let _id: String = try container.decode(String.self, forKey: ._id)
let index: Int = try container.decode(Int.self, forKey: .index)
let name: String = try container.decode(String.self, forKey: .name)
let englishName: String = try container.decode(String.self, forKey: .englishName)
let englishMeaning: String = try container.decode(String.self, forKey: .englishMeaning)
let place: String = try container.decode(String.self, forKey: .place)
let count: Int = try container.decode(Int.self, forKey: .count)
self.init(_id: _id, index: index, name: name, englishName: englishName, englishMeaning: englishMeaning, place: place, count: count)
}}
Code for Database Queries
let surahs = try JSONDecoder().decode([Surah].self, from: data!)
DispatchQueue.global(qos: .background).async {
//background code
do {
if let db = App.shared.database {
try db.inBatch {
for item in surahs {
let doc = MutableDocument(data: item.dictionary)
doc.setString("users", forKey: "type")
doc.setValue(Keys._id, forKey: item._id)
// doc.setValue(Keys.englishName, forKey: item.englishName)
try db.saveDocument(doc)
let index = IndexBuilder.valueIndex(items:
ValueIndexItem.expression(Expression.property("_id")), ValueIndexItem.expression(Expression.property("type")))
try db.createIndex(index, withName: "TypeNameIndex")
print("saved user document \(doc.string(forKey: "englishName"))")
}
}
}
} catch let error {
DispatchQueue.main.async {
seal.reject(error)
}
}
DispatchQueue.main.async {
seal.fulfill(surahs)
}
}

Not sure what you mean by "primary key". You can always find a doc
by its id. The name of the field that contains it is Meta.id.
The field's value is mutableDoc.getId(). As you've noticed, you
can also explicitly set the id at creation
Couchbase doesn't store classes, it stores JSON documents. If you
have documents of different types (different internal structures,
analogous to different SQL tables), give them a type field and use
it in your query
Use Database.inBatch()
The same way you would convert any JSON document to a corresponding
class: gson, Jackson, Moshi, etc

Related

Filtering empty values from an API in swift

I'm trying to filter out empty and null values from an api in a json format in swift(UIKit).
The full data returns look like below but sometimes can contain null or empty values in the characteristic key. There is always going to be the same amount of keys.
//Cat
{
"breedname": "Persian",
"picture": "https://catimage.random.png",
"characteristic1": "Shy",
"characteristic2": "Hungry all the time"
"characteristic3": "Likes apples"
"characteristic4": "Grey color"
"characteristic5": "likes chin scratches"
}
{
"breedname": "Bengal",
"picture": "https://catimage.random.png",
"characteristic1": "Active",
"characteristic2": "Adventurous"
"characteristic3": ""
"characteristic4": ""
"characteristic5": ""
}
{
"breedname": "ragdoll",
"picture": "https://catimage.random.png",
"characteristic1": "Fiestey",
"characteristic2": "sharp claws"
"characteristic3": null
"characteristic4": null
"characteristic5": null
}
In order to filter null and empty values before showing in the UI, I have a Decodable class like below and a custom init class with the decodeifPresent method which changes null values to nill. However for empty values I just created a method which converts empty string values to nill. I'm not sure if there are better ways to handle empty and null data and filtering them out? I refer to all the Decodable keys in the UI so I cannot simply delete the keys themselves.
struct Cat: Decodable {
let breedName: String
let picture: String
let characteristic1 : String?
let characteristic2 : String?
let characteristic3 : String?
let characteristic4 : String?
let characteristic5 : String?
enum CodingKeys: String, CodingKey {
case breedName
case picture
case characteristic1
case characteristic2
case characteristic3
case characteristic4
case characteristic5
}
func checkEmpty(s: String?) -> String? {
if s == "" {
return nil
}
return s
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.breedName= try container.decode(String.self, forKey: .breedName)
self.picture = try container.decode(String.self, forKey: .picture)
self.characteristic1 = try container.decodeIfPresent(String.self, forKey: .characteristic1)
self.characteristic2 = try container.decodeIfPresent(String.self, forKey: .characteristic2)
self.characteristic3 = try container.decodeIfPresent(String.self, forKey: .characteristic3)
self.characteristic4 = try container.decodeIfPresent(String.self, forKey: .characteristic4)
self.characteristic5 = try container.decodeIfPresent(String.self, forKey: .characteristic5)
self.characteristic1 = checkEmpty(s: self.characteristic1)
self.characteristic2 = checkEmpty(s: self.characteristic2)
self.characteristic3 = checkEmpty(s: self.characteristic3)
self.characteristic4 = checkEmpty(s: self.characteristic4)
self.characteristic5 = checkEmpty(s: self.characteristic5)
One solution is to check for empty in a function defined in an extension to String
extension String {
func emptyAsNil() -> String? {
self.isEmpty ? nil : self
}
}
Then you could do all in one step in the init
self.characteristic1 = try container.decodeIfPresent(String.self, forKey: .characteristic1)?.emptyAsNil()
But perhaps a better solution is to gather all those properties in a collection like an array or a dictionary. Here I have chosen an array
struct Cat: Decodable {
let breedName: String
let picture: String
var characteristics: [String]
}
and then in the init we add only non-nil, non-empty values to the array
if let value = try container.decodeIfPresent(String.self, forKey: .characteristic1), !value.isEmpty {
characteristics.append(value)
}
or another way is to loop over the keys
let keys: [CodingKeys] = [.characteristic1,
.characteristic2,
.characteristic3,
.characteristic4,
.characteristic5]
for key in keys {
if let value = try container.decodeIfPresent(String.self, forKey: key), !value.isEmpty {
characteristics.append(value)
}
}

How to decode DynamicKeys & CodingKeys in the same container?

Consider the following JSON: I'm trying to decode the "teams" object.
let jsonString = """
{
"Superheroes":{
"Marvel":"107",
"DC":"106"
},
"teams":{
"106":{
"name":"Marvel",
"Superheroes":{
"890":{
"name":"Batman"
}
}
},
"107":{
"name":"DC",
"Superheroes":{
"891":{
"name":"Wonder Woman"
}
}
}
}
}
"""
I have tried something like this:
struct SuperheroResponse: Decodable {
let teams: [Team]
private enum CodingKeys: String, CodingKey {
case teams = "teams"
}
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let teamContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.teams)
print(teamContainer.allKeys.count)
let tempArray: [Team] = []
for key in teamContainer.allKeys {
let decodedObject = try teamContainer.decode(Team.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
teams = tempArray
}
}
struct Team: Decodable {
let name: String
}
I thought that first I would get the teams container, map over the keys and go on from there. Problem is teamContainer.allKeys.count is always zero.
Also the following line, results in following error: Cannot convert value of type 'SuperheroResponse.DynamicCodingKeys' to expected argument type 'SuperheroResponse.CodingKeys'
let decodedObject = try teamContainer.decode(Team.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
Finally I decode it as follows:
let jsonData = Data(jsonString.utf8)
let decodedResult = try! JSONDecoder().decode(SuperheroResponse.self, from: jsonData)
dump(decodedResult)
Any help would be appreciated. Ideally I would like something like SuperheroResponse -> [Team],
Team -> name, [Superhero], Superhero -> name
You just have a couple of minor mistakes. You're almost there.
The team container is keyed by DynamicCodingKeys:
let teamContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, // <=
forKey: .teams)
And the Teams can be decoded as using the key you're given:
let decodedObject = try teamContainer.decode(Team.self, forKey: key)
Also, tempArray needs to be var:
var tempArray: [Team] = []
Or replace that loop with a map:
teams = try teamContainer.allKeys.map {
try teamContainer.decode(Team.self, forKey: $0)
}
All together:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let teamContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .teams)
teams = try teamContainer.allKeys.map {
try teamContainer.decode(Team.self, forKey: $0)
}
}

Subclass currently has no primary key relationship

I have JSON that looks like this, which returns a list of Posts:
[
{
"id" : 1,
"message": "Hello"
"urls" : {
"png" : "https://example.com/image.png",
"jpg" : "https://example.com/image.jpg",
"gif" : "https://example.com/image.gif"
}
}
]
As you can see, I need to make two classes. One for the parent object (Post), and one for the object "urls" (PostUrls).
I've done that like so:
class Post: Object, Decodable {
#objc dynamic var id = 0
#objc dynamic var message: String? = nil
#objc dynamic var urls: PostUrls? = nil
override static func primaryKey() -> String? {
return "id"
}
private enum PostCodingKeys: String, CodingKey {
case id
case message
case urls
}
convenience init(id: Int, message: String, urls: PostUrls) {
self.init()
self.id = id
self.message = message
self.urls = urls
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostCodingKeys.self)
let id = try container.decode(Int.self, forKey: .id)
let message = try container.decode(String.self, forKey: .message)
let urls = try container.decode(PostUrls.self, forKey: .urls)
self.init(id: id, message: message, urls: urls)
}
required init() {
super.init()
}
}
And
#objcMembers class PostUrls: Object, Decodable {
dynamic var png: String? = nil
dynamic var jpg: String? = nil
dynamic var gif: String? = nil
private enum PostUrlsCodingKeys: String, CodingKey {
case png
case jpg
case gif
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostUrlsCodingKeys.self)
png = try container.decodeIfPresent(String.self, forKey: .png)
jpg = try container.decodeIfPresent(String.self, forKey: .jpg)
gif = try container.decodeIfPresent(String.self, forKey: .gif)
super.init()
}
required init() {
super.init()
}
}
But, the problem is that I have no relationship between Post and PostUrls, since there is no primary key to connect the two. Further, this also means that I currently won't be able to control duplicates inside the PostUrls table.
So my question is: how can I create a relationship between the two tables, and prevent duplicates in the PostUrls table?
In this case, you do have a relationship between those objects. Object Post contains object PostUrls. Realm does not require a primary key to have this kind of relationship, because it creates a primary key behind the scenes. So it uses it, even though you can't access it.
To manually set a primaryKey you have to override a func called primaryKey()
#objcMembers class DBFilterModel: Object {
// MARK: Properties
dynamic var id: Int = 0
override public static func primaryKey() -> String? {
return "id"
}
}
This way you tell to realm that you want this property to be used as a Unique Key.
To prevent duplicating them, there are 2 ways. First - try to find an object with that id already existing in your database, if it exists - don't create it.
Second - by adding conflicts handlers to Realm's save methods. You can set that objects with same ID's will be just modified, but not duplicated. Or you could say that you want to throw an error when you try to insert a duplicated object.
realm.add(objects, update: update ? .modified : .error)
The question has two questions within
How do you create a relationship between the two 'tables'
prevent duplicates
Let me address 1)
Start with a class to hold the image type (.jpg etc) and then the url
class ImageUrlClass: Object {
#objc dynamic var imageType = ""
#objc dynamic var imageUrl = ""
}
and then the main class which handles decoding
class Post: Object, Decodable {
#objc dynamic var id: Int = 0
#objc dynamic var message: String = ""
let urlList = List<ImageUrlClass>()
...edited for brevity
convenience init(id: Int, message: String, urls: [String: String]) {
self.init()
self.id = id
self.message = message
//create a ImageUrlClass from each dictionary entry
for url in urls {
let key = url.key
let value = url.value
let aUrl = ImageUrlClass(value: ["imageType": key, "imageUrl": value])
self.urlList.append(aUrl)
}
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostCodingKeys.self)
let id = try container.decode(Int.self, forKey: .id)
let message = try container.decode(String.self, forKey: .message)
let urls = try container.decode([String: String].self, forKey: .urls)
self.init(id: id, message: message, urls: urls)
}
}
The above will create a Post object with a List property that contains the image types and urls (a List behaves very similar to an array)
You could further this by adding a LinkingObjects property to the ImageUrlClass which would automagically create an inverse relationship to the Post object when objects are added to the List. Not sure if you need that but it's available.
You can this do this to print out the post properties
let decoder = JSONDecoder()
let post = try! decoder.decode(Post.self, from: encodedData)
print(post.id)
print(post.message)
for url in post.urlList {
let a = url.imageType
let b = url.imageUrl
print(a,b)
}
which would results in an output like this
1
Hello
jpg https://example.com/image.jpg
png https://example.com/image.png
gif https://example.com/image.gif

Change variable of all elements inside an array if Set contains it

I know this is a silly question but I don't know which will be the more performant solution. I have an array of ListItem. ListItem has a boolean value isSelected. I also have Set<ListItem>. I want to change that boolean to true if array has an element which is also inside Set. How can I achieve this with the best performance?
My Set and Array:
var selectedItems = Set<ListItem>()
var array: [ListItem] = []
List Item:
class ListItem: Codable ,Equatable, Hashable {
let wrapperType: String
let kind: String?
let trackId: Int?
let artistId: Int?
let collectionId: Int?
let artistName, collectionName, trackName: String?
let trackViewUrl: String?
let artworkUrl30, artworkUrl60,artworkUrl100: String?
let releaseDate: String?
let primaryGenreName: String?
var isSelected: Bool = false
enum CodingKeys: String, CodingKey {
case wrapperType, kind
case artistId
case collectionId
case trackId
case artistName, collectionName, trackName
case trackViewUrl
case artworkUrl30, artworkUrl60, artworkUrl100, releaseDate, primaryGenreName
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
wrapperType = try container.decode(String.self, forKey: .wrapperType)
print(wrapperType)
kind = try container.decodeIfPresent(String.self, forKey: .kind)
trackId = try container.decodeIfPresent(Int.self, forKey: .trackId)
collectionId = try container.decodeIfPresent(Int.self, forKey: .collectionId)
artistId = try container.decodeIfPresent(Int.self, forKey: .artistId)
artistName = try container.decodeIfPresent(String.self, forKey: .artistName)
collectionName = try container.decodeIfPresent(String.self, forKey: .collectionName)
trackName = try container.decodeIfPresent(String.self, forKey: .trackName)
trackViewUrl = try container.decodeIfPresent(String.self, forKey: .trackViewUrl)
artworkUrl30 = try container.decodeIfPresent(String.self, forKey: .artworkUrl30)
artworkUrl100 = try container.decodeIfPresent(String.self, forKey: .artworkUrl100)
artworkUrl60 = try container.decodeIfPresent(String.self, forKey: .artworkUrl60)
releaseDate = try container.decodeIfPresent(String.self, forKey: .releaseDate)
primaryGenreName = try container.decodeIfPresent(String.self, forKey: .primaryGenreName)
}
static func ==(lhs: ListItem, rhs: ListItem) -> Bool {
return lhs.trackName == rhs.trackName
}
func hash(into hasher: inout Hasher) {
if trackId != nil {
hasher.combine(trackName)
} else if collectionId != nil {
//AudioBooks Doesn't have TrackId
hasher.combine(collectionId)
} else {
print("Both TrackId && Collection Id is null")
}
}
}
Because these are reference types, if it is guaranteed that there is only one instance of any unique ListItem, it would be sufficient to just set isSelected to false for each item in your array, and then set isSelected to true for each item in your selectedItems.
array.forEach { $0.isSelected = false }
selectedItems.forEach { $0.isSelected = true }
If there can be more than one instance of an item, you are going to have to iterate the items in your array and check if the Set contains them. Fortunately, contains is O(1) for a Set:
array.forEach { $0.isSelected = selectedItems.contains($0) }
Note: It is vital that the hashValue be equal for ListItems that are equal, or this all breaks down. Your hash(into:) function is currently using more fields than your == function, so it is possible to generate a different hashValue for equal ListItems. Fix this to ensure that a proper hashValue is generated.

Decode custom JSON with Decodable + Realm Swift

From the server I have a big JSON returned that looks something like this:
{
"id": "123",
"status": "ok",
"person": {
"administration": {
"name": "John"
}
},
"company": {
"name": "Test"
}
}
I have a struct:
struct Info: Decodable, Object {
let id: String
let status: String
let personName: String
let companyName: String
}
It conforms to Decodable protocol and also is a Object (Realm entity).
My question is: Am I able somehow to decode the name of the person in personName? Something like person.administration.name.
I want the end Realm Object, to be a flat one and mostly all of the fields are strings.
Should I create separate structs for Person/Company without being Realm Objects and in decode method to set the corresponding value to "personName"?
let personName: String = try container.decode((Person.Administration.name).self, forKey: .personName)
You can simply use containers to decode nested data with Decodable, i.e.
struct Info: Decodable {
let id: String
let status: String
let personName: String
let companyName: String
enum CodingKeys: String, CodingKey {
case id, status
case person, administration
case company
case name
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
status = try values.decode(String.self, forKey: .status)
//Decoding personName
let person = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .person)
let administration = try person.nestedContainer(keyedBy: CodingKeys.self, forKey: .administration)
personName = try administration.decode(String.self, forKey: .name)
//Decoding companyName
let company = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .company)
companyName = try company.decode(String.self, forKey: .name)
}
}
Example:
I've decoded the JSON you provided above, i.e.
if let data = json.data(using: .utf8) {
let info = try? JSONDecoder().decode(Info.self, from: data)
print(info)
}
The output it gives is:
(id: "123", status: "ok", personName: "John", companyName: "Test")
You can separate out the CodingKeys for all the different levels as per your wish. I kept them at the same level for simplicity.
Suggestion: Try using the optional types with Codable. This is because the API response can be unexpected. And if you don't get any expected key-value pair, you might end up getting a nil while creating the object.
It is best practice to separate transport types you're parsing your JSON into and types to represent object in the storage.
But if you want to use this combined types you should do something like this:
struct Info: Decodable {
let id: String
let status: String
let personName: String
let companyName: String
// JSON root keys
private enum RootKeys: String, CodingKey {
case id, status, person, company
}
// Keys for "person" nested "object"
private enum PersonKeys: String, CodingKey {
case administration
}
// Keys for "administration" and "company"
private enum NamedKeys: String, CodingKey {
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.status = try container.decode(String.self, forKey: .status)
let personContainer = try container.nestedContainer(keyedBy: PersonKeys.self, forKey: .person)
let administrationContainer = try personContainer.nestedContainer(keyedBy: NamedKeys.self, forKey: .administration)
self.personName = try administrationContainer.decode(String.self, forKey: .name)
let companyContainer = try container.nestedContainer(keyedBy: NamedKeys.self, forKey: .company)
self.companyName = try companyContainer.decode(String.self, forKey: .name)
}
}
I separated keys into three different CodingKey types for some type safety, and to prevent accidental mixup.

Resources