Swift filter nested array of objects - swift

I have an array of Business objects. Each Business object contains an array of key-value pairs, one element of which can be a nested array of ContentBlocks objects.
var masterArray = [
Business(busName: "Dave's Cafe", busId: 1, website: "http://www.davescafe.com", latLong: (45.541, -45.609),
actions: [["title": "About Us", "contentId": "123", "actionType": "content"],
["title": "Website", "url": "http://www.davescafe.com", "actionType": "web"]],
contentBlocks:[
ContentBlock(busName: "Dave's Cafe", busId: 1, contentId: "123", title: "Testola!", body: "Hello there!")
]),
Business(busName:...
]
I can filter the array to return a specific Businesses matching a unique busId by using something like this:
let rtnArray = masterArray.filter{$0.busId == id}
if rtnArray.count == 1{
return rtnArray[0]
} else {
return // feedback that no matches were found
}
Additionally, I'd like to return a specific contentBlock by filtering on the unique contentId (if necessary I can also pass the busId of the Business 'owner'). I'm really struggling to move forward so any pointers in the right direction would be great.

Here's a solution to what I think you're asking:
var contentBlocks = masterArray
.flatMap{$0.contentBlocks}
.flatMap{$0}
.filter{$0.contentId == "123"}
Outputs a [ContentBlock] containing all ContentBlock objects that match the filter from within all Business objects.
The first flatMap makes the list of Businesses into an [ContentBlock?]
The flatMap flattens the [ContentBlock?] into a [ContentBlock]
The [ContentBlock] is filtered

When you are dealing with model and want to search the nested custom object of class.
Here is class example.
public final class Model1 {
// MARK: Properties
public var firstname: String?
public var lastname: String?
public var email: String?
public var availabilities: [Availability]?
public var username: String?
}
public final class Availability {
var date: String
var day : String
init(date: String, day: String) {
self.date = date
self.day = day
}
}
public final class AvailData {
var am: String?
var pm: String?
init(am: String?,pm: String?) {
self.am = am
self.pm = pm
}
}
let make an array of Model1.
var arrData = [Model1]()
In my case, I want to search in availabilities array. So I put below logic and it work perfectly.
let searchData = arrData.filter({ singleObj -> Bool in
let result = singleObj.availabilities?.filter({ $0.date == "2019/09/17" })
return result?.count > 0 ? true : false
})
This swift code will return only those records which match 2019/09/17

Try this:
// The busId and contentId you want to find
let busId = 1
let contentId = "123"
let contentBlocks = masterArray.flatMap {
$0.contentBlocks.filter { $0.busId == busId && $0.contentId == contentId }
}

Related

How do you pass data dynamically is a Swift array?

Im creating a tinder like swipe app and I need a new CardData(name: "", age: "") to be created depending on how many profiles I pass through from my database. The number of cards will change. I need the number of cards created to match the the value of the results default. I have looked for the solution for a while and can't find it anywhere.
import UIKit
var nameArrayDefault = UserDefaults.standard.string(forKey: "nameArray")!
var ageArrayDefault = UserDefaults.standard.string(forKey: "ageArray")!
var nameArray = nameArrayDefault.components(separatedBy: ",")
var ageArray = ageArrayDefault.components(separatedBy: ",")
var results = UserDefaults.standard.string(forKey: "results")!
struct CardData: Identifiable {
let id = UUID()
let name: String
let age: String
static var data: [CardData] {[
CardData(name: “\(nameArray[0])”, age: “\(ageArray[0])”),
CardData(name: “\(nameArray[1])”, age: “\(ageArray[1])"),
CardData(name: “\(nameArray[2])”, age: “\(ageArray[2])”)
]}
}
You should initiate the array of CardData objects only the first time and update it after that. You can do the following
var _data = [CardData]()
var data: [CardData] {
if _data.isEmpty() {
self.initiateData()
}
return _data
}
// Initiate data for the first time only
func initiateData() -> [CardData] {
// Check that nameArray has the same size as ageArray
// If the size is different then the data are not valid.
guard nameArray.count == ageArray.count else {
return []
}
// For each of names inside nameArray create the corresponding CardData object
self.nameArray.forEach({ (index, item)
self._data.append(CardData(name: item, age: ageArray[index]))
})
}

Inner filtering of array doesn't filter swift

I am trying to filter an array of structs that has array. Below are the data structures I am using. I want the inner array filtered also but it doesn't work
var objects = [SomeObject]() //array of objects
var filteredObject = [SomeObject]() //filtered array
var isSearching = false
struct SomeObject {
var sectionName: String
var sectionObjects : [History]
}
struct History {
var firstName: String
var lastName: Int
}
func searchBar(_ text: String) {
filteredObject = objects.filter({ (obj: SomeObject) -> Bool in
return obj.sectionObjects.filter { $0.firstName.contains(text.lowercased())}.isEmpty
})
print("====", filteredObject, "fill===")
}
let history = History(firstName: "John", lastName: 1)
let anotherHistroy = History(firstName: "Dee", lastName: 2)
let historyArray = [history, anotherHistroy]
let newObject = SomeObject(sectionName: "Section 1", sectionObjects: historyArray)
objects.append(newObject)
searchBar("Jo") // printing of filtered object should not have another history in it's sectionObjects
You might be looking for something like this:
func searchBar(_ text: String) {
filteredObject = []
for var ob in objects {
ob.sectionObjects = ob.sectionObjects.filter {
$0.firstName.contains(text)
}
if !ob.sectionObjects.isEmpty {
filteredObject.append(ob)
}
}
print("====", filteredObject, "fill===")
}
Could perhaps be done more elegantly with reduce(into:), but on the whole it is best to start simply by saying exactly what you mean. You can tweak as desired to take account of case sensitivity.

RealmSwift + ObjectMapper managing String Array (tags)

What I need to represent in RealmSwift is the following JSON scheme:
{
"id": 1234,
"title": "some value",
"tags": [ "red", "blue", "green" ]
}
Its a basic string array that I'm stumbling on. I'm guessing in Realm I need to represent "tags" as
dynamic id: Int = 0
dynamic title: String = ""
let tags = List<MyTagObject>()
making tags its own table in Realm, but how to map it with ObjectMapper? This is how far I got...
func mapping(map: Map) {
id <- map["id"]
title <- map["title"]
tags <- map["tags"]
}
... but the tags line doesn't compile of course because of the List and Realm cannot use a [String] type.
This feels like a somewhat common problem and I'm hoping someone who has faced this can comment or point to a post with a suggestion.
UPDATE 1
The MyTagObject looks like the following:
class MyTagObject: Object {
dynamic var name: String = ""
}
UPDATE 2
I found this post which deals with the realm object but assumes the array has named elements rather than a simple string.
https://gist.github.com/Jerrot/fe233a94c5427a4ec29b
My solution is to use an ObjectMapper TransformType as a custom method to map the JSON to a Realm List<String> type. No need for 2 Realm models.
Going with your example JSON:
{
"id": 1234,
"title": "some value",
"tags": [ "red", "blue", "green" ]
}
First, create an ObjectMapper TransformType object:
import Foundation
import ObjectMapper
import RealmSwift
public struct StringArrayTransform: TransformType {
public init() { }
public typealias Object = List<String>
public typealias JSON = [String]
public func transformFromJSON(_ value: Any?) -> List<String>? {
guard let value = value else {
return nil
}
let objects = value as! [String]
let list = List<String>()
list.append(objectsIn: objects)
return list
}
public func transformToJSON(_ value: Object?) -> JSON? {
return value?.toArray()
}
}
Create your 1 Realm model used to store the JSON data:
import Foundation
import RealmSwift
import ObjectMapper
class MyObjectModel: Object, Mappable {
#objc dynamic id: Int = 0
#objc dynamic title: String = ""
let tags = List<MyTagObject>()
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
id <- map["id"]
title <- map["title"]
tags <- (map["tags"], StringArrayTransform())
}
}
Done!
This line is the magic: tags <- (map["tags"], StringArrayTransform()). This tells ObjectMapper to use our custom StringArrayTransform I showed above which takes the JSON String array and transforms it into a Realm List<String>.
First of all we should assume that our model extends both Object and Mappable.
Let's create a wrapper model to store the primitive (String) type:
class StringObject: Object {
dynamic var value = ""
}
Then we describe corresponding properties and mapping rules for the root model (not the wrapper one):
var tags = List<StringObject>()
var parsedTags: [String] {
var result = [String]()
for tag in tags {
result.append(tag.value)
}
return result
}
override static func ignoredProperties() -> [String] {
return ["parsedTags"]
}
func mapping(map: Map) {
if let unwrappedTags = map.JSON["tags"] as? [String] {
for tag in unwrappedTags {
let tagObject = StringObject()
tagObject.value = tag
tags.append(tagObject)
}
}
}
We need a tags property to store and obtain the data about tags from Realm.
Then a parsedTags property simplifies extraction of tags in the usual array format.
An ignoredProperties definition allows to avoid some failures with Realm while data savings (because of course we can't store non-Realm datatypes in the Realm).
And at last we are manually parsing our tags in the mapping function to store it in the Realm.
It will work if your tags array will contains a Dictionary objects with a key: "name"
{
"id": 1234,
"title": "some value",
"tags": [ ["name" : "red"], ... ]
}
If you cannot modify JSON object, I recommend you to map json to realm programmatically.
for tagName in tags {
let tagObject = MyTagObject()
tagObject.name = tagName
myObject.tags.append(tagObject)
}
Follow this code
import ObjectMapper
import RealmSwift
//Create a Model Class
class RSRestaurants:Object, Mappable {
#objc dynamic var status: String?
var data = List<RSData>()
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
status <- map["status"]
data <- (map["data"], ListTransform<RSData>())
}
}
//Use this for creating Array
class ListTransform<T:RealmSwift.Object> : TransformType where T:Mappable {
typealias Object = List<T>
typealias JSON = [AnyObject]
let mapper = Mapper<T>()
func transformFromJSON(_ value: Any?) -> Object? {
let results = List<T>()
if let objects = mapper.mapArray(JSONObject: value) {
for object in objects {
results.append(object)
}
}
return results
}
func transformToJSON(_ value: Object?) -> JSON? {
var results = [AnyObject]()
if let value = value {
for obj in value {
let json = mapper.toJSON(obj)
results.append(json as AnyObject)
}
}
return results
}
}

Realm query nested object

Hello everyone I'm having difficulties archiving one thing with a query of nested object. I have two realm object Championship and Game.
class Championship: Object {
dynamic var id: Int = 0
dynamic var name: String = ""
let games = List<Game>
override static func primaryKey() -> String? {
return "id"
}
}
class Game: Object {
dynamic var id: Int = 0
dynamic var homeTeamName: String = ""
dynamic var awayTeamName: String = ""
dynamic var status: String = "" //"inprogress", "finished", "scheduled"
override static func primaryKey() -> String? {
return "id"
}
}
And basically I want to retrieve all championships that have games with status "inprogress", so what I'm doing to archive that is:
let realm = try! Realm()
realm.objects(Championship.self).filter("ANY games.status = 'inprogress'")
What that query is doing is giving me all championships that have at least one game with that status, but is also giving me all the the games from that championship, but actually I just want the games with "inprogress" status.
There any way for doing that?
You could take two approaches here. If you want all games with inprogress status, you could write this:
let inProgressGames = realm.objects(Championship.self)
.flatMap { $0.games }
.filter { $0.status == "inprogress" }
This will return you all games in progress inside [Game] array. flatMap is used to combine all games from all championships and filter is used to filter all games with inProgress status.
If you want championships where every game is inprogress you could write:
let inProgressChampionships = realm.objects(Championship.self).filter {
let inProgressGames = $0.games.filter { $0.status == "inprogress"}
return $0.games.count == inProgressGames.count
}
This will return array of Championship where each game is inprogress.
Other that that, I would recommend using enum for game status instead of hard-coded strings.

Swift Filtering a Collection Based on Another Collection

I have two classes "Receipe" and "Incredient". A receipe can have a list of incredients. Now, I want that when I pass in a list of incredient I should get all the receipies back which contain that incredient. Here is what I have:
How can I filter the receipies based on the passed in incrediants.
class Recipe {
var name :String!
var incredients :[Incredient]!
init(name :String, incredients :[Incredient]) {
self.name = name
self.incredients = incredients
}
}
class Incredient {
var name :String!
init(name :String) {
self.name = name
}
}
var incredientsToSearchFor = [Incredient(name:"Salt"),Incredient(name :"Sugar")]
var receipe1 = Recipe(name: "Receipe 1", incredients: [Incredient(name: "Salt"),Incredient(name :"Pepper"),Incredient(name :"Water"),Incredient(name :"Sugar")])
var receipe2 = Recipe(name: "Receipe 2", incredients: [Incredient(name: "Salt"),Incredient(name :"Pepper"),Incredient(name :"Water"),Incredient(name :"Sugar")])
var receipe3 = Recipe(name: "Receipe 3", incredients: [Incredient(name :"Pepper"),Incredient(name :"Water"),Incredient(name :"Sugar")])
var receipies = [receipe1,receipe2,receipe3] // list of all the recipies
func getRecipiesByIncrediants(incredients :[Incredient]) -> [Recipe] {
// WHAT TO DO HERE
return nil
}
let matchedRecipies = getRecipiesByIncrediants(incredientsToSearchFor)
A few things need to change.
First, these are data models, so they should probably be struct types instead of class. This allows you to remove the initializer and optionals too:
struct Recipe : Equatable {
let name: String
let ingredients: [Ingredient]
}
struct Ingredient : Equatable {
let name : String
}
You'll notice I also made them Equatable. This is so you can use contains to find them in an array. Without it, contains won't know whether two of these types are equal.
To conform to Equatable, just add the == methods:
func ==(lhs: Ingredient, rhs: Ingredient) -> Bool {
return lhs.name == rhs.name
}
func ==(lhs: Recipe, rhs: Recipe) -> Bool {
return lhs.name == rhs.name && lhs.ingredients == rhs.ingredients
}
Creating your data is roughly the same. I fixed some spelling errors and changed var to let since these values don't change:
let ingredientsToSearchFor = [Ingredient(name:"Salt"), Ingredient(name :"Sugar")]
let recipe1 = Recipe(name: "Recipe 1", ingredients: [Ingredient(name: "Salt"), Ingredient(name :"Pepper"), Ingredient(name :"Water"), Ingredient(name :"Sugar")])
let recipe2 = Recipe(name: "Recipe 2", ingredients: [Ingredient(name: "Salt"), Ingredient(name :"Pepper"), Ingredient(name :"Water"), Ingredient(name :"Sugar")])
let recipe3 = Recipe(name: "Recipe 3", ingredients: [Ingredient(name :"Pepper"), Ingredient(name :"Water"), Ingredient(name :"Sugar")])
let recipes = [recipe1, recipe2, recipe3] // list of all the recipes
Now on to the filtering. There's more efficient ways to do this, but for ease of reading you can use filter and reduce:
func getRecipesByIngredients(incredients :[Ingredient]) -> [Recipe] {
return recipes.filter { recipe in
incredients.reduce(true) { currentValue, ingredient in
return currentValue && (recipe.ingredients.contains(ingredient))
}
}
}
filter returns a new array containing only elements where the block returns true. reduce consolidates an entire array into one value (in this case true or false). So we iterate through each recipe, and check whether all of the specified ingredients are in it.
To call the method:
let matchedRecipes = getRecipesByIngredients(ingredientsToSearchFor)
This returns recipes 1 and 2 because both contain salt and sugar.
If you want recipes that contain salt OR sugar, use reduce(false) and change && to ||.