Remove duplicate elements from object array - ios

I am attempting to remove duplicate elements of my Transaction object. The Transactions are being loaded from Firestore and displayed onto a UTableView. I tried to follow this answer [here][1] however I got an error that budgetData is not hashable. Is there a way I can remove duplicate Transactions that have the same "transId" and return an updated array of budgetdata?
var budgetData: [Transaction] = []
func loadCatTransactions(){
if let catId = self.categoryId{
guard let user = Auth.auth().currentUser?.uid else { return }
print("userFromLoadChat::\(user)")
db.collection("users").document(user).collection("Transactions")
.whereField("catId", isEqualTo: catId)
.getDocuments() {
snapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
self.budgetData.removeAll()
for document in snapshot!.documents {
let data = document.data()
let title = data["name"] as? String ?? ""
let date = data["date"] as? String ?? ""
let amount = data["amount"] as? Double ?? 0
let id = data["transId"] as? String ?? ""
let trans = Transaction(catId:catId,title: title, dateInfo: date, image: UIImage.groceriesIcon, amount: amount)
self.budgetData.append(trans)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
func uniq<S : Sequence, T : Hashable>(source: S) -> [T] where S.Iterator.Element == T {
var buffer = [T]()
var added = Set<T>()
for elem in source {
if !added.contains(elem) {
buffer.append(elem)
added.insert(elem)
}
}
return buffer
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.budgetData.count
}
struct Transaction {
var catId : String? = nil
var title: String
var dateInfo: String
var image: UIImage
var amount: Double
var annualPercentageRate: Double?
var trailingSubText: String?
var uid: String?
var outOfString: String?
var category: String?
var dictionary:[String:Any]{
return [
"title": title,
"dateInfo":dateInfo,
"amount":amount,
"annualPercentageRate":annualPercentageRate,
"trailingSubText":trailingSubText,
"uid": uid,
"outOfString": outOfString,
"category": category
]
}
}
[1]: Removing duplicate elements from an array in Swift

You need to make Transaction object Hashable. Try this
struct Transaction{
var transId: String
}
extension Transaction: Hashable{
static func ==(lhs: Transaction, rhs: Transaction) -> Bool {
return lhs.transId == rhs.transId
}
}
var budgetData = [Transaction(transId: "a"), Transaction(transId:"c"),
Transaction(transId: "a"), Transaction(transId: "d")]
var tranSet = Set<Transaction>()
budgetData = budgetData.filter { (transaction) -> Bool in
if !tranSet.contains(transaction){
tranSet.insert(transaction)
return true
}
return false
}

Related

Saving document ID so it can be used throughout the ViewController

I am new to Swift development, so sorry if this is a stupid question. I'm having issues with saving the Firestore document ID to the cell of my to do.
My goal:
Save the document ID of the to-do so it can be used in my ChangeButton protocol.
The app is a to-do list-style app. The changeButton refers to changing the button from an empty circle to a filled circle.
My cellForRowAt in my mainViewController:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 && indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "inputCell", for: indexPath) as! InputCell
cell.delegate = self
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath) as! TaskCell
let current = sections[indexPath.section].items[indexPath.row]
cell.taskNameLabel.text = current.name
if current.checked {
cell.checkBoxOutlet.setBackgroundImage(#imageLiteral(resourceName: "checkBoxFILLED "), for: UIControl.State.normal)
} else {
cell.checkBoxOutlet.setBackgroundImage(#imageLiteral(resourceName: "checkBoxOUTLINE "), for: UIControl.State.normal)
}
cell.delegate = self
cell.items = sections[indexPath.section].items
cell.indexSection = indexPath.section
cell.indexRow = indexPath.row
cell.itemID = sections[indexPath.section].items[indexPath.row].itemID
// print("cell.itemID is \(cell.itemID)")
// print("sections.itemID is \(sections[indexPath.section].items[indexPath.row].itemID)")
return cell
}
}
My changeButton function in mainViewController
func changeButton(state: Bool, indexSection: Int?, indexRow: Int?, itemID: String?) {
print("The item ID is \(itemID)")
print("The item ID section is \(sections[indexSection!].items[indexRow!].itemID)")
sections[indexSection!].items[indexRow!].checked = state
print("Line 175 ID is \(itemID)")
if let itemID = itemID {
let itemRef = db.collection(K.FStore.lists).document(currentListID!).collection(K.FStore.sections).document("\(indexSection!)").collection(K.FStore.items).document(itemID)
if sections[indexSection!].items[indexRow!].checked {
itemRef.updateData([
K.FStore.isChecked : true,
K.FStore.checkedBy: currentUserID!
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
} else {
itemRef.updateData([
K.FStore.isChecked : false
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
} else {
print("No item ID")
}
tableView.reloadData()
}
My loadItems and loadFunctions in my mainViewController
func loadItems(listID: String, section: Int) {
let itemRef = db.collection(K.FStore.lists).document(listID).collection(K.FStore.sections).document("(section)").collection(K.FStore.items)
var itemArray = Task
itemRef.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
let name = document.data()["name"] as? String
let isChecked : Bool = (document.data()["isChecked"] != nil)
let newItem = Task(name: name ?? "FIREBASE ERROR", isChecked: isChecked)
itemArray.append(newItem)
// print(newItem.checked)
}
}
// print(itemArray)
self.sections[section].items = itemArray
self.tableView.reloadData()
}
}
//MARK: - Load sections
func loadSections(listID: String) {
let listRef = db.collection(K.FStore.lists).document(listID)
listRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
let sectionNames = document.data()!["sections"] as? [String]
if let sectionNames = sectionNames {
for (index, item) in sectionNames.enumerated() {
let newSection = Section(name: item, isExpanded: true, items: [])
self.sections.append(newSection)
self.loadItems(listID: listID, section: index)
}
}
self.tableView.reloadData()
} else {
print("Document does not exist")
}
}
}
My Task class
class Task {
var name = ""
var checked = false
var date = Date()
var category: String
var number: Int
var itemID: String?
My TaskCell
protocol ChangeButton {
func changeButton(state: Bool, indexSection: Int?, indexRow: Int?, itemID: String?)
}
class TaskCell: UITableViewCell {
#IBAction func checkBoxAction(_ sender: Any) {
// print("The item ID is \(itemID)")
if items![indexRow!].checked {
delegate?.changeButton(state: false, indexSection: indexSection!, indexRow: indexRow!, itemID: itemID)
print("Line 22 \(itemID)")
} else {
delegate?.changeButton(state: true, indexSection: indexSection!, indexRow: indexRow!, itemID: itemID)
print("Line 25 \(itemID)")
}
}
#IBOutlet weak var taskNameLabel: UILabel!
#IBOutlet weak var checkBoxOutlet: UIButton!
var delegate: ChangeButton?
var indexSection: Int?
var indexRow: Int?
var tasks: [[Task]]?
var items: [Task]?
var itemID: String?
}
I am completely lost in how I can fix this. As you can see, I've tried a lot of print statements to figure out where the itemID can be loaded.
This is what I get back from those print statements:
Line 25 nil
The item ID is nil
The item ID section is nil
Line 175 ID is nil
No item ID
Line 22 nil
Please let me know if I forgot to include anything, and sorry for this extremely long post.
Thanks a ton,
Matt
Firstly,
We don't trust cells because it's reusable so we need a static class.
When cell will be reuse you can lost your data
I create some examples. Maybe it'll be helpful
// This is our model
class Task {
var name = ""
var checked = false
var date = Date()
var category: String
var number: Int
var itemID: String?
public init() {
self.category = ""
self.number = 0
}
}
// Extension for init from firebase response
extension Task {
convenience init(with firebase: [String: Any]) {
self.init()
self.name = (firebase["name"] as? String) ?? ""
}
}
// We create service for document
// We use this service like an API
final class DocumentService {
static let shared = DocumentService()
private let database: FirebaseDatabase
private var tasks: [[Task]] = []
public init(database: FirebaseDatabase = FirebaseDatabase()) {
self.database = database
}
func load(in section: Int, completion: #escaping (([Task]) -> Void)) {
database.loadData(section: section) { [unowned self] tasks in
self.tasks[section] = tasks.map(Task.init)
completion(self.tasks[section])
}
}
func check(at indexPath: IndexPath, isChecked: Bool) {
tasks[indexPath.section][indexPath.row].checked = isChecked
}
}
// We create firebase database class we can add some features in here
final class FirebaseDatabase {
func loadData(section: Int, completion: #escaping (([[String: Any]]) -> Void)) {
// TODO: firebase load data
let response: [[String: Any]] = [
["name": "Stackoverflow"]
]
completion(response)
}
}
final class TestController: UIViewController {
private let service = DocumentService.shared
override func viewDidLoad() {
super.viewDidLoad()
service.load(in: 0) { tasks in
// TODO
}
}
}
Thank you for your answer, #Vicaren. Fortunately, the solution was more simple than that. In the end, I found that I forgot to pass in the itemID argument in the loadItems() function. Thank you.

Swift - How to write complex objects to Firebase realtime database

I'm new to the Firebase realtime database and relatively new to Swift in general. I am attempting to build a song request app in which users can create events for guests to request songs from the Spotify API. I'm trying to write an Event object to Firebase, which contains nested objects and arrays of different types. However, when it writes to the database, it only writes the strings and none of the arrays or objects. What is the best way to write all this information to the Firebase Database in a nested structure, so that whenever users add song requests, I can edit the array of requests for the given event in firebase.
Here is my code:
Event.swift
struct Event: Codable{
var code: String
var name: String
var host: String
var description: String
var hostUserId: String
var guestIds: [String]
var requests: [Request]
var queue: [Request]
var played: [Request]
//private var allowExplicit: Bool
//private var eventLocation
init(code: String, name: String, host: String, description: String, hostUserId: String){
self.code = code
self.name = name
self.host = host
self.description = description
self.hostUserId = hostUserId
self.guestIds = []
self.requests = []
self.queue = []
self.played = []
}
func toAnyObject() -> Any{
var guestIdsDict: [String:String] = [:]
for id in guestIds{
guestIdsDict[id] = id
}
var requestsDict: [String: Any] = [:]
for request in requests{
requestsDict[request.getId()] = request.toAnyObject()
}
var queueDict: [String: Any] = [:]
for request in queue{
queueDict[request.getId()] = request.toAnyObject()
}
var playedDict: [String: Any] = [:]
for request in played{
playedDict[request.getId()] = request.toAnyObject()
}
return [
"code": code,
"name": name,
"host": host,
"description": description,
"hostUserId": hostUserId,
"guestIds": guestIdsDict,
"requests": requestsDict,
"queue":queueDict,
"played":playedDict
]
}
}
Request.swift
struct Request: Codable{
private var name: String
private var id: String
private var explicit: Bool
private var album: Album
private var artists: [Artist]
private var likes: Int
init(name: String, id: String, explicit: Bool, album: Album, artists: [Artist]){
self.name = name
self.id = id
self.explicit = explicit
self.album = album
self.artists = artists
self.likes = 1
}
func toAnyObject() -> Any{
var artistsDict: [String:Any] = [:]
for artist in artists {
artistsDict[artist.id] = artist.toAnyObject()
}
return [
"name": name,
"id": id,
"explicit": explicit,
"album": album.toAnyObject(),
"artists": artistsDict,
"likes": likes
]
}
mutating func like(){
self.likes += 1
}
mutating func unlike(){
self.likes -= 1
if(self.likes < 0){
self.likes = 0
}
}
mutating func setLikes(count: Int){
self.likes = count
}
func getLikes() -> Int{
return self.likes
}
func getName() -> String{
return self.name
}
func getId() -> String{
return self.id
}
func getExplicit() -> Bool{
return self.explicit
}
func getAlbum() -> Album {
return self.album
}
func getImages() -> [Image] {
return self.album.images
}
func getArtists() -> [Artist] {
return self.artists
}
func getArtistString() -> String{
var artistString = ""
for (i, artist) in self.artists.enumerated(){
artistString += artist.name
if(i != self.artists.endIndex-1){
artistString += ", "
}
}
return artistString
}
}
Album.swift
struct Album: Codable{
let name: String
let images: [Image]
func toAnyObject() -> Any{
var imagesDict: [String: Any] = [:]
for image in images{
imagesDict[image.url] = image.toAnyObject()
}
return [
"name": name,
"images": imagesDict
]
}
}
Artist.swift
struct Artist: Codable{
let id: String
let name: String
func toAnyObject() -> Any{
return ["id": id, "name": name]
}
}
Image.swift
struct Image: Codable{
let height: Int
let url: String
let width: Int
func toAnyObject() -> Any{
return ["height": height, "url": url, "width": width]
}
}
As you are using Codable, you can create a dic out of it as follows:
Step 1: Add this extension to your code
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
Step 2: Write below code in your Struct (this you have to do in every struct or you can modify code as per your need).
func createDic() -> [String: Any]? {
guard let dic = self.dictionary else {
return nil
}
return dic
}
Now with the help of struct obj, call createDic() method and you will get a dictionary.
And you can send this dictionary to the firebase.
FULL CODE EXAMPLE:
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
}
struct LoginModel: Codable {
let email: String
let password: String
func createDic() -> [String: Any]? {
guard let dic = self.dictionary else {
return nil
}
return dic
}
}
Please comment if you have any questions.
Happy to help!

TableView loaded before fetching data Firebase / Swift

i have a FireBase database, inside i have a table of products and another table of orders with ids of these products, what i am trying to do is to get products from table of products based on ids inside table of orders, since FireBase will only allow me to get the products one by one , my tableview is loaded before i get all products that are referenced inside the orders table.
heres how i did that :
struct Product: Decodable, Encodable{
var id: String?
var ref: String
var description: String
var type: String
var price: String
var qtyOrdred:Int?
}
struct Order:Decodable, Encodable {
var id: String?
var isValide: Bool
var madeBy: String
var info: String?
var ordredProd: [OrderedProduct]
}
struct OrderedProduct:Decodable, Encodable {
var id: String
var qty: Int
}
func getData(completion: #escaping ([Product])->Void){
var allProduct = [Product]()
for product in orderedProduct {
getProductWithKey(qty: product.qty, key: product.id) { (p) in
print(p.ref)
allProduct.append(p)
}
}
}
func getProductWithKey(qty: Int,key: String, completion: #escaping (Product)->Void) {
Database.database().reference().child("products").child(key).observeSingleEvent(of: .value) { (snap) in
if let productObject = snap.value as? [String: Any]
{
if let ref = productObject["ref"],
let price = productObject["price"],
let type = productObject["type"],
let description = productObject["description"],
let id = productObject["id"]{
let p = Product(id: id as? String, ref: ref as! String, description: description as! String, type: type as! String, price: price as! String, qtyOrdred: qty)
completion(p)
}
}
}
}
i call it like this :
override func viewWillAppear(_ animated: Bool) {
self.getData { (ps) in
print(ps)
self.tableView.reloadData()
}
}
The problem is that it always print an empty array, and my tableview data never changes
You don't return from getData completion , you need a dispatch group
let g = DispatchGroup()
func getData(completion: #escaping ([Product])->Void){
var allProduct = [Product]()
for product in orderedProduct {
g.enter()
getProductWithKey(qty: product.qty, key: product.id) { (p) in
print(p.ref)
allProduct.append(p)
g.leave()
}
}
g.notify(queue:.main) {
completion(allProduct)
}
}
Your getData function is returning as soon as the for loop is finished. As the call inside the loop is async there isn't any data when the loop finishes.
Instead of reloading the table just insert rows as they arrive.
for product in orderedProduct {
getProductWithKey(qty: product.qty, key: product.id) { [weak self] (p) in
guard let self = self else { return }
allProduct.append(p)
guard let index = allProduct.firstIndex(of: p) else { return }
self.tableView.insertRow(at: IndexPath(row: index, section: 0))
}
}

Call func with various struct

I want to create one func which i can used with various struct.
I have several struct and I want use one func with all my struct.
I work with Firestore and want use this one func to access the Firestore.
My first struct:
struct Profile {
var name = ""
var surname = ""
var email = ""
var dictionary: [String: Any] {
return [
"name": name,
"surname": surname,
"email": email
]
}
}
extension Profile: DocumentSerializable {
init?(dictionary: [String: Any], id: String) {
let name = dictionary["name"] as? String ?? ""
let surname = dictionary["surname"] as? String ?? ""
let email = dictionary["email"] as? String ?? ""
self.init(name: name,
surname: surname,
email: email)
}
}
My second struct:
struct FavoriteList {
var favoriteList: [String]
var id: String
var dictionary: [String: Any] {
return [
"favoriteList": favoriteList,
"id": id
]
}
}
extension FavoriteList: DocumentSerializable {
init?(dictionary: [String : Any], id: String) {
let favoriteList = dictionary["favorite"] as? [String] ?? [""]
let id = id
self.init(favoriteList: favoriteList, id: id)
}
}
And my func which I used now to load data from firestore:
func observeQuery() {
guard let query = query else { return }
let time = DispatchTime.now() + 0.5
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
if let snapshot = snapshot {
DispatchQueue.main.asyncAfter(deadline: time) {
let profileModels = snapshot.documents.map { (document) -> Profile in
if let profileModel = Profile(dictionary: document.data(), id: document.documentID) {
return profileModel
} else {
fatalError("Error!")
}
}
self.profile = profileModels
self.document = snapshot.documents
self.tableView.reloadData()
}
}
}
}
So how I can make func observeQuery to use my structs Profile or FavouriteList?
You can use Generic Functions :
func observeQuery<T>(someObject: T) {
if someObject is Profile {
//do something
} else if someObject is FavouriteList {
//do something
}
}

UiCollectionView Cell Selection Error

I have used Alamofire and SwiftyJSON To Populate the UiCollectionview and its working fine but didSelectItemAtIndexPath function shows array out of index thought I have printed the array count and it's not empty
any suggestion
here is my code:-
The model
import Foundation
class ProductModel {
private var _ProductItemId: String!
private var _ProductMainCategory: String!
private var _ProductCategoryId: String!
private var _ProductName: String!
private var _ProductItemNo: String!
private var _ProductAvalaibility: String!
private var _ProductSeoDesc: String!
private var _ProductImageURL: String!
private var _ProductBrand_ID: String!
private var _ProductCat_name: String!
//Level 1
private var _ProductTotalQuantity : String!
private var _Productprice : String!
private var _ProductSalePrice : String!
private var _ProductWeightName : String!
private var _ProductCode : String!
var ProductItemId : String {
return _ProductItemId
}
var ProductMainCategory : String {
return _ProductMainCategory
}
var ProductCategoryId : String {
return _ProductCategoryId
}
var ProductName : String {
return _ProductName
}
var ProductItemNo : String {
return _ProductItemNo
}
var ProductAvalaibility : String {
return _ProductAvalaibility
}
var ProductSeoDesc : String {
return _ProductSeoDesc
}
var ProductImageURL : String {
return _ProductImageURL
}
var ProductBrand_ID: String {
return _ProductBrand_ID
}
var ProductCat_name: String {
return _ProductCat_name
}
//Level 1
var ProductTotalQuantity : String {
return _ProductTotalQuantity
}
var Productprice : String {
return _Productprice
}
var ProductSalePrice : String {
return _ProductSalePrice
}
var ProductWeightName : String {
return _ProductWeightName
}
var ProductCode : String {
return _ProductCode
}
//Initilizer
init(ProductImageURL : String, ProductName : String, Productprice : String, ProductSalePrice : String)
{
self._ProductName = ProductName
self._ProductImageURL = ProductImageURL//
//Level 1
self._Productprice = Productprice//
self._ProductSalePrice = ProductSalePrice//
}
My CollectionView Delegates and Data sources
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ProductCell", forIndexPath: indexPath)as? ProductCell {
let _prod: ProductModel!
_prod = prod [indexPath.row]
cell.configureCell(_prod)
return cell
}
else{
return UICollectionViewCell()
}
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let prodDetail: ProductModel!
prodDetail = prod[indexPath.row] //error Array index out of range
print(prodDetail.Productprice)
performSegueWithIdentifier("productDetailSegue", sender: prodDetail)
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//if inSearchMode{
//return filteredProd.count
// }
return prod.count
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
Calling API and Parsing
Alamofire.request(.POST, "http://www.picknget.com/webservice/index.php/Home/filter_grocery_product_practice/", parameters: parameterDictionary as? [String : AnyObject])
.responseJSON { response in
if let value = response.result.value {
let json = JSON(value)
print(json)
if let _statusCode = json["status"].string {
// print("the ststus code is ", _statusCode)
if (_statusCode == "1"){
self.parseJSON(json)
}
if (_statusCode == "0"){
SwiftSpinner.hide({
self.callAlert("OOP's", _msg: "No More Product is available in this section right now")
})
}
}
//print ("json result ", json)
}
}.responseString { response in
//print("response ",response.result.value)
}
}
func parseJSON(json: JSON) {
for result in json["cat"].arrayValue {
let name = result["Name"]
let aString: String = "\(result["ImageURL"])"
let product_Image_Url = aString.stringByReplacingOccurrencesOfString("~", withString: "http://www.picknget.com", options: NSStringCompareOptions.LiteralSearch, range: nil)
let price = result["cat_price"][0]["Price"].string
let SalePrice = result["cat_price"][0]["SalePrice"].string
let product = ProductModel(ProductImageURL: "\(product_Image_Url)", ProductName: "\(name)", Productprice: "\(price!)", ProductSalePrice: "\(SalePrice!)")
prod.append(product)
}
print("########")
print(prod.count)
dispatch_async(dispatch_get_main_queue(),{
self.productCollect.reloadData()
});
}
According your comments, I believe the issue is related to how you set the obtained Products for the Collection View.
It's very likely that the function parseJSON executes on a secondary thread. This is actually, the same execution context of the completion handler of method responseJSON.
Within function parseJSON you have this statement:
prod.append(product)
Here, prod should not be a global variable or not a member variable of the view controller! Make it a local variable in function parseJSON!
Your view controller should have a property of this array as well, e.g. products. This serves as the "model" of the view controller. It will be accesses only from the main thread.
In parseJSON assign the view controller the products as follows:
func parseJSON(json: JSON) {
var tmpProducts: [Product] = []
for result in json["cat"].arrayValue {
let name = result["Name"]
let aString: String = "\(result["ImageURL"])"
let product_Image_Url = aString.stringByReplacingOccurrencesOfString("~", withString: "http://www.picknget.com", options: NSStringCompareOptions.LiteralSearch, range: nil)
let price = result["cat_price"][0]["Price"].string
let SalePrice = result["cat_price"][0]["SalePrice"].string
let product = ProductModel(ProductImageURL: "\(product_Image_Url)", ProductName: "\(name)", Productprice: "\(price!)", ProductSalePrice: "\(SalePrice!)")
tmpProducts.append(product)
}
dispatch_async(dispatch_get_main_queue(),{
self.products = tmpProducts // assuming `self` is the ViewController
self.productCollect.reloadData()
});
}
Note: you need to change your Data Source Delegates accordingly, e.g. accessing the "model" (self.products.count) etc.

Resources