My app consists on an image of various foods, in which the user taps the image and adds this food into a Set<Food>.
I want to show all items from this Set inside the class called Favorites, as a: Text("You like: \(favorites.comidas)") but I can't manage to make it work
class Favorites: ObservableObject {
var foods: Set<Food>
}
class Favorites: ObservableObject {
var foods: Set<Food>
init() {
// load the saved data
foods = []
}
func contains(_ food: Food) -> Bool {
foods.contains(food)
}
func add(_ food: Food) {
objectWillChange.send()
foods.insert(food)
save()
}
func delete(_ food: Food) {
objectWillChange.send()
foods.remove(food)
save()
}
}
struct Food: Identifiable, Hashable {
var id: Int
let name: String
let foodImage: [String]
// Equatable
static func == (lhs: Food, rhs: Food) -> Bool {
lhs.id == rhs.id
}
}
#EnvironmentObject var favorites: Favorites
let food: Food
var body: Some View {
Image(food.foodImage[0])
.onTapGesture {
if favorites.contains(food) {
favorites.delete(food)
} else {
favorites.add(food)
}
}
}
You haven't shown your Food structure, but I will assume it has a property, name.
ListFormatter is your friend with a task like this. Its string(from:[]) function takes an array and returns it in a nicely formatted list. You can use map to get an array of name strings from your set.
For the input array ["pizza","tacos","chocolate"] it will give "pizza, tacos and chocolate"
var favoriteList: String {
let formatter = ListFormatter()
let favorites = formatter.string(from:self.favorites.foods.map{$0.name})
return favourites ?? ""
}
Then you can use this function in a Text view:
Text("You like \(self.favoriteList)")
Note that a Set is unordered, so it might be nice to sort the array so that you get a consistent, alphabetical order:
var favoriteList: String {
let formatter = ListFormatter()
let favorites = formatter.string(from:self.favorites.foods.map{$0.name}.sorted())
return favourites ?? ""
}
Thanks to a tip from Leo Dabus in the comments, in Xcode 13 and later you can just use .formatted -
var favoriteList: String {
return self.favorites.foods.map{$0.name}.sorted().formatted() ?? ""
}
Related
I am having a behavior relay in my view model that is used as a source of data. Its defined like this:
var posts: BehaviorRelay<[PostModel]>
It is initialized with data through the network, and it initializes tableView normally when I bind data to it.
Now, if I try to change say, the like status of a post here, like this (this is also in my view model):
private func observeLikeStatusChange() {
self.changedLikeStatusForPost
.withLatestFrom(self.posts, resultSelector: { ($1, $0) })
.map{ (posts, changedPost) -> [PostModel] in
//...
var editedPosts = posts
editedPosts[index] = changedPost // here data is correct, index, changedContact
return editedPosts
}
.bind(to: self.posts)
.disposed(by: disposeBag)
}
So with this, nothing happens. If I remove the element from editedPosts, the tableView updates correctly and removes the row.
PostModel struct conforms to Equatable, and it requires all properties to be the same at the moment.
In my view controller, I create datasource like this:
tableView.rx.setDelegate(self).disposed(by: disposeBag)
let dataSource = RxTableViewSectionedAnimatedDataSource<PostsSectionModel>(
configureCell: { dataSource, tableView, indexPath, item in
//...
return postCell
})
postsViewModel.posts
.map({ posts in
let models = posts.map{ PostCellModel(model: $0) }
return [PostsSectionModel(model: "", items: models)]
}) // If I put debug() here, this is triggered and I get correct section model with correct values
.bind(to: self.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
So, as I said, I am getting correct values, but configureCell is not triggered. What I am doing wrong here?
EDIT:
Here is PostCellModel:
import Foundation
import RxDataSources
typealias PostsSectionModel = AnimatableSectionModel<String, PostCellModel>
struct PostCellModel : Equatable, IdentifiableType {
static func == (lhs: PostCellModel, rhs: PostCellModel) -> Bool {
return lhs.model.id == rhs.model.id
}
var identity: Int {
return model.id
}
var model: PostModel
}
and a PostModel:
struct PostModel: Codable, CellDataModel, Equatable {
static func == (lhs: PostModel, rhs: PostModel) -> Bool {
return
lhs.liked == rhs.liked &&
rhs.title == lhs.title &&
lhs.location == rhs.location &&
lhs.author == rhs.author &&
lhs.created == rhs.created
}
let id: Int
let title: String
let location: String?
let author: String
let created: Int
let liked:Bool
}
You have defined your Equatable conformance incorrectly in the PostCellModel. Because of that, the system is unable to tell whether a cell model has changed... Remove your manually defined ==(lhs:rhs:) and let the system generate them for you and you should be fine...
typealias PostsSectionModel = AnimatableSectionModel<String, PostCellModel>
struct PostCellModel : Equatable, IdentifiableType {
var identity: Int {
return model.id
}
var model: PostModel
}
struct PostModel: Codable, CellDataModel, Equatable {
let id: Int
let title: String
let location: String?
let author: String
let created: Int
let liked:Bool
}
I have a collection of teams in my Firebase Cloud Firestore each of which have players. I have previously saved each of these players in an array inside the team document however found this to be inefficient and slow the app dramatically. After some research I have converted this array into a sub collection called players and want to access it the same way.
Here is my team and player model (simplified):
struct DBTeam: Identifiable, Codable{
#DocumentID var id: String?
var name: String
var colour: Int
var dateCreated: Date
var players: [DBPlayer] = [DBPlayer]()
var userID: String?
}
struct DBPlayer: Identifiable, Equatable, Codable{
var id: String = UUID().uuidString
var name: String
static func ==(lhs: DBPlayer, rhs: DBPlayer) -> Bool {
return lhs.name == rhs.name && lhs.id == rhs.id
}
}
I then use this repository to get all the data and feed it into my view model:
class TeamRepository: ObservableObject{
var db = Firestore.firestore()
#Published var teams = [DBTeam]()
init(){
loadData()
}
func loadData(){
let userID = Auth.auth().currentUser?.uid
db.collection("teams")
.order(by: "name")
.whereField("userID", isEqualTo: userID ?? "")
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot{
self.teams = querySnapshot.documents.compactMap { document in
do{
let x = try document.data(as: DBTeam.self)
return x
}
catch{
print(error)
}
return nil
}
}
}
}
}
I have tried running this code again to see if by any chance it would work, but it didn't. By using these new sub collections, is this the most efficient way to store a bunch of players to one team and then reference them? If not any other suggestions? And how would I access these sub collections and store them into the array of players found in the DBTeam struct?
I have a custom class defined as follows :
class DisplayMessage : NSObject {
var id : String?
var partner_image : UIImage?
var partner_name : String?
var last_message : String?
var date : NSDate?
}
Now I have an array myChats = [DisplayMessage]?. The id field is unique for each DisplayMessage object. I need to check my array and remove all duplicates from it, essentially ensure that all objects in the array have a unique id. I have seen some solutions using NSMutableArray and Equatable however I'm not sure how to adapt them here; I also know of Array(Set(myChats)) however that doesn't seem to work for an array of custom objects.
Here is an Array extension to return the unique list of objects based on a given key:
extension Array {
func unique<T:Hashable>(map: ((Element) -> (T))) -> [Element] {
var set = Set<T>() //the unique list kept in a Set for fast retrieval
var arrayOrdered = [Element]() //keeping the unique list of elements but ordered
for value in self {
if !set.contains(map(value)) {
set.insert(map(value))
arrayOrdered.append(value)
}
}
return arrayOrdered
}
}
for your example do:
let uniqueMessages = messages.unique{$0.id ?? ""}
You can do it with a set of strings, like this:
var seen = Set<String>()
var unique = [DisplayMessage]
for message in messagesWithDuplicates {
if !seen.contains(message.id!) {
unique.append(message)
seen.insert(message.id!)
}
}
The idea is to keep a set of all IDs that we've seen so far, go through all items in a loop, and add ones the IDs of which we have not seen.
Here is an Array extension to return the unique list of objects based on a keyPath:
extension Array {
func uniques<T: Hashable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return reduce([]) { result, element in
let alreadyExists = (result.contains(where: { $0[keyPath: keyPath] == element[keyPath: keyPath] }))
return alreadyExists ? result : result + [element]
}
}
}
Usage:
myChats.uniques(by: \.id)
Create a free duplicate version of an Array, using equality comparisons based on a given key
public extension Sequence {
public func uniq<Id: Hashable >(by getIdentifier: (Iterator.Element) -> Id) -> [Iterator.Element] {
var ids = Set<Id>()
return self.reduce([]) { uniqueElements, element in
if ids.insert(getIdentifier(element)).inserted {
return uniqueElements + CollectionOfOne(element)
}
return uniqueElements
}
}
public func uniq<Id: Hashable >(by keyPath: KeyPath<Iterator.Element, Id>) -> [Iterator.Element] {
return self.uniq(by: { $0[keyPath: keyPath] })
}
}
public extension Sequence where Iterator.Element: Hashable {
var uniq: [Iterator.Element] {
return self.uniq(by: { (element) -> Iterator.Element in
return element
})
}
}
Usage
let numbers = [1,2,3,4,5,6,7,1,1,1,]
let cars = [Car(id:1), Car(id:1), Car(id:2)]
numbers.uniq
cars.uniq(by: { $0.id})
cars.uniq(by: \Car.id)
cars.uniq(by: \.id)
I have a class called Person:
class Person{
let name: String?
let areas: [Area]
}
And Area consisting of the following:
class Area{
let id: String
let name: String
}
I've created a "people" array and what I want to do is check the "people" array if area.name = "Brooklyn". If it doesn't remove that person from the "people" array.
Here is what I have tried but it doesn't work:
var people = [Person]()
for s in people{
for a in s.preferredArea{
if a.areaName != "Brooklyn"{
let index = people.indexOf(s)
people.removeAtIndex(index!)
}
}
}
Can anyone point me into the right direction please?
To use indexOf, you'll have to make Person conforming the protocol Equatable:
class Person: Equatable {
let name: String
var area: [Area]
init(name: String) {
self.name = name
self.area = []
}
}
func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name &&
lhs.area == rhs.area // You'll have to make Area equatable too
}
Having this done, your code just works fine. If you don't want to use this method, you can use:
people.enumerate().forEach {
if !$0.1.area.contains({ $0.name == "Brooklyn" }) {
people.removeAtIndex($0.0)
}
}
Or if you can use filter to create a new array:
var newPeople = people.filter { $0.area.contains { $0.name == "Brooklyn" } }
I have an array, with custom objects.
I Would like to pop the repeated objects, with the repeated properties:
let product = Product()
product.subCategory = "one"
let product2 = Product()
product2.subCategory = "two"
let product3 = Product()
product3.subCategory = "two"
let array = [product,product2,product3]
in this case, pop the product2 or product3
Here is an Array extension to return the unique list of objects based on a given key:
extension Array {
func unique<T:Hashable>(map: ((Element) -> (T))) -> [Element] {
var set = Set<T>() //the unique list kept in a Set for fast retrieval
var arrayOrdered = [Element]() //keeping the unique list of elements but ordered
for value in self {
if !set.contains(map(value)) {
set.insert(map(value))
arrayOrdered.append(value)
}
}
return arrayOrdered
}
}
using this you can so this
let unique = [product,product2,product3].unique{$0.subCategory}
this has the advantage of not requiring the Hashable and being able to return an unique list based on any field or combination
You can use Swift Set:
let array = [product,product2,product3]
let set = Set(array)
You have to make Product conform to Hashable (and thus, Equatable) though:
class Product : Hashable {
var subCategory = ""
var hashValue: Int { return subCategory.hashValue }
}
func ==(lhs: Product, rhs: Product) -> Bool {
return lhs.subCategory == rhs.subCategory
}
And, if Product was a NSObject subclass, you have to override isEqual:
override func isEqual(object: AnyObject?) -> Bool {
if let product = object as? Product {
return product == self
} else {
return false
}
}
Clearly, modify those to reflect other properties you might have in your class. For example:
class Product : Hashable {
var category = ""
var subCategory = ""
var hashValue: Int { return [category, subCategory].hashValue }
}
func ==(lhs: Product, rhs: Product) -> Bool {
return lhs.category == rhs.category && lhs.subCategory == rhs.subCategory
}
If Product conforms to Equatable, where a product is equal based on it's subcategory (and you don't care about order), you can add the objects to a set, and take an array from that set:
let array = [product,product2,product3]
let set = NSSet(array: array)
let uniqueArray = set.allObjects
or
let array = [product,product2,product3]
let set = Set(array)
let uniqueArray = Array(set)
If your class conforms to protocol Hashable and you would like to keep the original array order you can create an extension as follow:
extension Array where Element: Hashable {
var uniqueElements: [Element] {
var elements: [Element] = []
for element in self {
if let _ = elements.indexOf(element) {
print("item found")
} else {
print("item not found, add it")
elements.append(element)
}
}
return elements
}
}
class Product {
var subCategory: String = ""
}
let product = Product()
product.subCategory = "one"
let product2 = Product()
product2.subCategory = "two"
let product3 = Product()
product3.subCategory = "two"
let array = [product,product2,product3]
extension Product : Hashable {
var hashValue: Int {
return subCategory.hashValue
}
}
func ==(lhs: Product, rhs: Product)->Bool {
return lhs.subCategory == rhs.subCategory
}
let set = Set(array)
set.forEach { (p) -> () in
print(p, p.subCategory)
}
/*
Product one
Product two
*/
if an item is part of set or not doesn't depends on hashValue, it depends on comparation. if your product conform to Hashable, it should conform to Equatable. if you need that the creation of the set depends solely on subCategory, the comparation should depends solely on subCategory. this can be a big trouble, if you need to compare your products some other way
Here is a KeyPath based version of the Ciprian Rarau' solution
extension Array {
func unique<T: Hashable>(by keyPath: KeyPath<Element, T>) -> [Element] {
var set = Set<T>()
return self.reduce(into: [Element]()) { result, value in
guard !set.contains(value[keyPath: keyPath]) else {
return
}
set.insert(value[keyPath: keyPath])
result.append(value)
}
}
}
example usage:
let unique = [product, product2, product3].unique(by: \.subCategory)