How do I concisely update a highly nested object model using swift? - arrays

I am writing a function which will dig into the deepest level of a object hierarchy and update a particular value. For reference, this is how messy a regular iteration turned out to be:
func selectedStateUpdated(data: MyObject, tabId: String, rowId: String){
for currIndex in 0..<self.model.data[0].data[0].filters.count{
if(self.model.data[0].data[0].filters[currIndex].headerName == data.headerName){
for tabIdx in 0..<model.data[0].data[0].filters[currIndex].data.count {
if(self.model.data[0].data[0].filters[currIndex].data[tabIdx].id == tabId){
for rowItemIdx in 0..<self.model.data[0].data[0].filters[currIndex].data[tabIdx].data.count {
if(self.model.data[0].data[0].filters[currIndex].data[tabIdx].data[rowItemIdx].id == rowId){
self.model.data[0].data[0].filters[currIndex].data[tabIdx].data[rowItemIdx].isSelected.toggle()
}
}
}
}
}
}
}
What is the best way to dig this deep into my data model and update a particular attribute? I am trying to update the model object which resides in the class of this function as a global object. Given that iterable objects in my object model are immutable in for-loops, is there a better way of approaching this while making sure I iterated into the proper

At its core, this is a data structure problem. You're doing a linear search through all your data to find something with a specific headerName, tabId, rowId. It's complicated, messy and slow (O(headerCount * tabCount * rowCount)!)
If dictionaries were used, this could just be:
self.model.data[0].data[0]
.filters[headerName]
.data[tabId]
.data[rowId]
.isSelected
.toggle()
If the arrays must stay, then you can clean this up a lot by extracting common parts to named local variables. Inverting the if statements (in this case, into guard statements`) also saves a ton on nesting:
func selectedStateUpdated(headerName: String, tabId: String, rowId: String){
let data = self.model.data[0].data[0] // TODO: name me better
for currIndex in 0..<data.filters.count {
let filter = data.filters[currIndex]
guard filter.headerName == headerName else { return }
for tabIdx in 0..<filter.data.count {
let tab = filter.data[tabIdx]
guard tab.id == tabId else { continue }
for rowItemIdx in 0..< tab.data.count {
let rowItem = tab.data[rowItemIdx]
guard rowItem.id == rowId else { continue }
self.model.data[0].data[0]
.filters[currIndex]
.data[tabIdx]
.data[rowItemIdx]
.isSelected
.toggle()
}
}
}
}
It could be further improved by using zip to get indices and items in one shot:
func selectedStateUpdated(headerName: String, tabId: String, rowId: String){
let data = self.model.data[0].data[0] // TODO: name me better
for (currIndex, filter) in zip(data.filters.indices, data.filters) {
guard filter.headerName == headerName else { return }
for (tabIdx, tab) in zip(filter.data.indices, filter.data) {
guard tab.id == tabId else { continue }
for (rowItemIdx, rowItem) in zip(tab.data.indices, tab.data) {
guard rowItem.id == rowId else { continue }
self.model.data[0].data[0]
.filters[currIndex]
.data[tabIdx]
.data[rowItemIdx]
.isSelected
.toggle()
}
}
}
}

Basically what you are doing when you say ".id == tabId" is a filter function. This code was quite messy. But i think i managed to figure it out:
func selectedStateUpdated(data: [MyObject], tabId: String, rowId: String) {
let filteredResult = self.model.data[0].data[0].filters
.filter({$0.headerName == data.headerName &&
$0.data.filter({$0.id == tabId}) &&
$0.data.filter({$0.id == tabId &&
$0.data.filter({$0.id == rowId})
})
})
if filteredResult.count > 0 {
filteredResult.first!.isSelected.toggle()
}
}

Related

How we can achieve this Filter in Swift

How we can achieve this Filter in Swift.
I have exactly same problem and i am trying this way and i found this solution on stack overflow
but this is written in Javascript and i need code in Swift language.
Getting this error
Cannot convert value of type '[Model]' to closure result type
'GetModel'
My Code and Model
extension Array where Element == GetModel{
func matching(_ text: String?) -> [GetModel] {
if let text = text, text.count > 0{
return self.map{
$0.data.filter{
$0.name.lowercased().contains(text.lowercased())
}
}
}else{
return self
}
}
}
// MARK: - GetModel
struct GetModel: Codable {
let id: Int
let name: String
var data: [Model]
}
// MARK: - Model
struct Model:Codable {
let id: Int
let name: String
var isSelected: Bool? = nil
}
You are making two mistakes. First you are using map but you should be using filter. Second you are using filter when you should be using contains(where:). Note you can. use localizedStandardCompare instead of lowercasing your string.
Note: You shouldn't check if your string count is greater than zero. String has an isEmpty property exactly for this purpose.
To check whether a collection is empty, use its isEmpty property
instead of comparing count to zero. Unless the collection guarantees
random-access performance, calculating count can be an O(n) operation.
extension RangeReplaceableCollection where Element == GetModel {
func matching(_ text: String?) -> Self {
guard let text = text, !text.isEmpty else { return self }
return filter { $0.data.contains { $0.name.localizedStandardContains(text) } }
}
}
edit/update:
If you need to filter your GetModal data:
extension RangeReplaceableCollection where Element == GetModel, Self: MutableCollection {
func matching(_ text: String?) -> Self {
guard let text = text, !text.isEmpty else { return self }
var collection = self
for index in collection.indices {
collection[index].data.removeAll { !$0.name.localizedStandardContains(text) }
}
collection.removeAll(where: \.data.isEmpty)
return collection
}
}

How to get a tuple from two lists of elements

I have two lists of UIViews, some of that UIViews have an accessibilityIdentifier most of them are nil.
I'm searching for a way to generate a tuple(or something) with the two UIViews from the lists that have the same accessibilityIdentifier.
The lists are not sorted or something.
Is there a way to not iterate multiple times through the second list to find every pair?
for view in firstViewList {
if view.accessibilityIdentifier != nil {
for secondView in secondViewList {
if secondView.accessibilityIdentifier != nil && secondView.accessibilityIdentifier == view.accessibilityIdentifier {
viewPairs.append((firstView: view, secondView: secondView))
}
}
}
}
I think this is not very efficient.
Make a dict that indexes both view lists by their ID, filter out the ones where the ID is nil, and then use the keys common to both dicts to create a new dict that indexes pairs of same-id views.
Here's a rough example (which I haven't compiled myself).
func makeDictByAccessibilityID(_ views: [UIView]) -> [AccessibilityID: UIView] {
return Dictionary(uniqueKeysWithValues:
firstViewList
.lazy
.map { (id: $0.accessibilityIdentifier, view: $0) }
.filter { $0.id != nil }
)
}
viewsByAccessibilityID1 = makeDictByAccessibilityID(firstViewList)
viewsByAccessibilityID2 = makeDictByAccessibilityID(secondViewList)
commonIDs = Set(viewsByAccessibilityID1.keys).intersecting(
Set(viewsByAccessibilityID2.keys)
)
let viewPairsByAccessibilityID = Dictionary(uniqueKeysWithValues:
commonIDs.lazy.map { id in
// Justified force unwrap, because we specifically defined the keys as being available in both dicts.
(key: id, viewPair: (viewsByAccessibilityID1[id]!, viewsByAccessibilityID2[id]!))
}
}
This runs in O(n) time, which is the best you can get for this problem.
I think you should first filtered your two arrays from the nil value then you can do like this
let tempfirstViewList = firstViewList.filter { (view) -> Bool in
view.accessibilityIdentifier != nil
}
var tempsecondViewList = secondViewList.filter { (view) -> Bool in
view.accessibilityIdentifier != nil
}
tempfirstViewList.forEach { (view) in
let filterdSecondViewList = tempsecondViewList.filter { (secondView) -> Bool in
secondView.accessibilityIdentifier == view.accessibilityIdentifier
}
if let sView = filterdSecondViewList.first {
viewPairs.append((firstView: view, secondView: sView))
//after add it to your tuple remove it from the temp array to not loop throw it again
if let index = tempsecondViewList.firstIndex(of: sView) {
tempsecondViewList.remove(at: index)
}
}
}
This solution creates an array of tuples with views from the first and second list respectively
var viewArray: [(UIView, UIView)]
firstViewList.forEach( { view in
if let identifier = view.accessibilityIdentifier {
let arr = secondViewList.filter( {$0.accessibilityIdentifier == identifier} )
arr.forEach( { viewArray.append((view, $0))})
}
})

Filter Custom Array time optimization

I have to filter array of articles based on a keyword(string) in article's description. Filtering is taking 2-3 seconds on 1500+ elements's array of type Article and 2000+ words in each article description.
I am using below code; i also tried predicate but didn't worked for me.
let searchResult = articlesList.filter {
let article = $0
let filterByName = article.title.lowercased().range(of: text.lowercased())
let filterByDescription = article.body.lowercased().range(of: (text.lowercased()))
if ((filterByName != nil) || filterByDescription != nil) {
if !articlesList.contains(article) {
articlesList.append(article)
}
}
else {
let index = articlesList.index(of: article)
if index != nil {
articlesList.remove(at: index!)
}
}
return false
}
I want filter time max to 0.3 seconds.
This is probably a bit lot faster
let searchResult = articles.filter{ $0.title.range(of: text, options: .caseInsensitive) != nil
|| $0.body.range(of: text, options: .caseInsensitive) != nil }
The problem is that you perform changes on the collection you are filtering. You shouldn't have articleList.remove() and articleList.append() in the filter function. The way filter works is that you just return true for every element that you want to keep in the filtered collection (in your case searchResult) and false for the rest.
Try this code:
let searchResult = articlesList.filter { article in
let filterByName = article.title!.lowercased().range(of: text.lowercased())
let filterByDescription = article.body!.lowercased().range(of: (text.lowercased()))
if ((filterByName != nil) || filterByDescription != nil){
return true
}
return false
}

.contains not working with custom object

I am using the below code to update my array. My requirement is that the new data does not exist, for that I am using the .contains method.
This method is not working properly when I change the viewcontroller and come again on same page. It returns false and I keep getting duplicate results due to that.
Is this an issue related to the custom object?
if let list = rooms as? [[String : AnyObject]]
{
// self.roomList = [Room]()
for item in list
{
let roomStr = json(from: item)
let roomObj : Room = Room(JSONString: roomStr!)!
if !self.roomList.contains(roomObj)
{
self.roomList.append(roomObj)
}
}
DispatchQueue.main.async {
//First sort by name and then sort by default room..
self.roomList.sort { $0.getRoomName()! < $1.getRoomName()! }
self.roomList.sort { $0.getDefaultRoom()! && !$1.getDefaultRoom()! }
self.LoadRoomsTableView.reloadData()
self.hideActivity()
}
}
Any idea how to solve it or any suggest on the effiecient way for add/update the data in array with swift.
You have to make sure your Room class/struct implements the protocol Equatable.
Example:
class Room {
let rooms: Int
let color: String
init(rooms: Int, color: String) {
self.rooms = rooms
self.color = color
}
}
extension Room: Equatable {
func ==(lhs: Room, rhs: Room) -> Bool {
return lhs.rooms == rhs.rooms && lhs.color == rhs.color
}
}
If you had a struct and Swift > 4.1+ you would not have this problem, as the == is kindly provided by Swift. Thanks Vadian for the reminder.

Filter nested arrays with objects

I have an array of Categories. Each Category instance has offers property.
class Category {
var offers : [Offer]?
var title : String?
var id : Int?
}
class Offer {
var type : String?
}
//global variable
var categories = [ categ1, categ2, ...]
How can I filter categories by offer.type ?
I already have tried:
return categories.map { (category) -> Category in
let offers = category.offers?.filter { $0.type == myType }
category.offers = offers
return category
}
It works but after calling function second time array becomes empty. Probably because offers were rewritten?
Then I have tried this (produced same wrong result):
var resultCategories = [Category]()
for category in categories {
guard let offers = category.offers else { continue }
var newOffers = [Offer]()
for offer in offers {
if offer.type == myType {
newOffers.append(offer)
}
}
category.offers = newOffers
resultCategories.append(category)
}
return resultCategories
You should simply filter all categories that have no offers equals to your type. You can achieve that by:
filter all your categories and
inside the filter check if current offers contains the myType
Code:
let filtered = categories.filter { category in
category.offers?.contains(where: { $0.type == myType }) ?? false
}
And note, that category.offers?.[...] is optional value, so the ?? false returns false as result if left part is nil.
UPD.
But I expected that categories will have only offers with type = "A". Maybe I did not described the question accurately.
You can achieve that by creating a new Category.
let filtered = categories.compactMap { category -> Category? in
guard let offers = category.offers?.filter({ $0.type == "A" }) else { return nil }
let other = Category()
other.offers = offers
return other
}
Also note, i'm using compactMap. It allows me to filter categories with empty or nil offers out.
You can use simple and easy filter(functional programming) instead of for-loop.
First filter category then check offers contains particular type or not(using equal to condition)
let data = categories.filter { ($0.offers?.contains(where: {$0.type == "yourtype"})) ?? false
}
If you want multiple filter, like one field from one model and second field from nested array, please che
let searchText = "a"
let filteredCategoryList = list.filter { category in
let categoryFilter = category.title?.range(of: searchText, options: .caseInsensitive) != nil
let offerFilter = category.offers?.contains(where: { $0.type?.range(of: searchText, options: .caseInsensitive) != nil
})
return categoryFilter || offerFilter ?? false
}

Resources