Whenever I add new data to Firebase or update the data in the Firebase console, my tableView data duplicates. The udemy tutorial I've followed never showed how to solve the issue. A Google search brought up my problem but non seemed to explain the root of the problem; I after the root of the issue and not just an answer. I'm learning as I go along but this one bugs me.
One answer mentioned the use of self.members.removeAll() but Im using Swift 3, where to put that and why?
My issue relates to this but that answer seemed vague and not really an answer. I have a configureCell() but what should be added?
I upvote most of my question's answers when someone explains and just adding an answer. What is happening and why, please? Do I need to reload/remove something in:
if let cell = tableView.dequeueReusableCellWithIdentifier("PostCell") as? PostCell{...}
Edited:
var expenses = [Expense]()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return expenses.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let expense = expenses[indexPath.row]
if let cell = tableView.dequeueReusableCell(withIdentifier: "expenseFeedCell") as? ExpenseFeedCell {
cell.selectionStyle = .none
cell.configureCell(expense: expense)
return cell
} else {
return ExpenseFeedCell()
}
}
And the Expense Class:
class Expense {
// Private variables
private var _date: Double!
private var _type: String!
private var _amount: Double!
private var _notes: String!
private var _expenseId: String!
// Setting up the getteres
var type: String {
return _type
}
var date: Double {
return _date
}
var amount: Double {
return _amount
}
var notes: String {
return _notes
}
var expenseId: String {
return _expenseId
}
init(type: String, date: Double, amount: Double, notes: String) {
self._type = type
self._date = date
self._amount = amount
self._notes = notes
}
init(expenseId: String, expenseData: [String: AnyObject]) {
self._expenseId = expenseId
if let type = expenseData["type"] as? String {
self._type = type
}
if let date = expenseData["date"] as? Double {
self._date = date
}
if let amount = expenseData["amount"] as? Double {
self._amount = amount
}
if let notes = expenseData["notes"] as? String {
self._notes = notes
}
}
}
If the firebase method you use to read data is called each time there is a new entry, you should remove the content of your data array before adding data on it.
So in the firebase method, start by having an empty array. After that you can treat with the snapchot and fill this same array.
Add a self.tableView.reloadData() to reload the view with new data in this array
Related
For instance say I have a CarModel
struct CarModel: Codable {
var numberPlate: String
var vin: String
var model: String
var fuel: Double
var position: Position
}
and I have a public method
extension CarModel {
var fuelString: String {
return fuelStringImplementation
}
}
but instead of having the implementation in the public method I hide it behind a private method.
private extension CarModel {
var fuelStringImplementation: String {
if fuel == 0.0 {
return "car_list_item_tank_empty".localizedString()
}
let fuelDouble = fuel*100
let finalString = String(format: "car_list_item_tank_status".localizedString(), fuelDouble)
return finalString
}
}
And for cellForAtIndexPath I can hide the implmentation in a private method like this
private func collectionViewImplementation(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MainImageCollectionViewCell.className, for: indexPath)
guard let model = viewModel.modelForIndex(index: indexPath.row) else {
assertionFailure("model is nil")
return cell
}
guard let mainImageCell = cell as? MainImageCollectionViewCell else {
assertionFailure("cell is not type MainImageCollectionViewCell")
return cell
}
mainImageCell.fill(with: model)
return mainImageCell
}
No, it does not matter if the method is private or public for performance advantage.
If we are talking about functions and performance improvement (not to mention to a body implementation of that function), we should look closer to the method dispatch type. There are some of them: dynamic, table and direct. And direct has the best performance, for example.
It's a huge theme, so I suggest you to read some articles about it on web. Like this, for example, https://developer.apple.com/swift/blog/?id=27.
BACKGROUND
I have a simple bill splitting app that allows the users to assign a meal item to multiple people or users. When the meal is assigned to multiple people, the price is divided accordingly. The meal (which contains an item name and a price are the rows and the users are the sections.
When I delete a row, I want to delete the row, and update or alter certain other values (I basically want to reassign the price to one less person when an item is deleted from a user). My data model is a multidimensional array. Here it is:
struct UserModel {
var name: String
var itemModels = [ItemModel]()
init(name: String, itemModels: [ItemModel]? = nil) {
self.name = name
if let itemModels = itemModels {
self.itemModels = itemModels
}
}
}
struct ItemModel {
var itemName: String
var price: Double
init(itemName: String, price: Double) {
self.itemName = itemName
self.price = price
}
}
class Data {
static var userModels = [UserModel]()
static var itemModels = [ItemModel]()
}
For example, in trailingSwipeActionsConfigurationForRowAt
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let delete = UIContextualAction(style: .destructive, title: "Delete") { (contextualAction, view, actionPerformed: #escaping (Bool) -> ()) in
let user = Data.userModels[indexPath.section].name
let item = Data.userModels[indexPath.section].itemModels[indexPath.row].itemName
let price = Data.userModels[indexPath.section].itemModels[indexPath.row].price
ItemModelFunctions.removeFromUser(from: indexPath.section, remove: indexPath.row)
var duplicate = Data.userModels.filter({$0.itemModels.contains(where: {$0.itemName == item})}).filter({$0.name != user})
for i in 0..<duplicate.count {
???
}
tableView.reloadData()
actionPerformed(true)
}
return UISwipeActionsConfiguration(actions: [delete])
}
The variable var duplicate returns an array of the other users who have the same item at the indexPath.row, but not the user(indexPath.section) who has the item. I know it sounds really confusing, but I can provide more code or print statements if needed.
Also in the for loop, I want to do something like this:
for i in 0..<duplicate.count {
let oldPrice = duplicate[i].price
let newPrice = oldPrice * duplicate.count
duplicate[i].price = newPrice
}
But I can't access the price. I believe need an indexPath.section and indexPath.row.
If you made it this far, thank you for taking the time. I feel like I need a nested loop, but I'm not sure how exactly to implement that. If there are any other easier ways to achieve this I'm open to any suggestions.
Thank you so much!
EDIT:
The marked answer worked! In case anyone else was having a similar issue this is what my final code looks like in the trailingSwipeActionsConfigurationForRowAt:
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let delete = UIContextualAction(style: .destructive, title: "Delete") { (contextualAction, view, actionPerformed: #escaping (Bool) -> ()) in
let item = Data.userModels[indexPath.section].itemModels[indexPath.row]
ItemModelFunctions.removeFromUser(from: indexPath.section, remove: indexPath.row)
let duplicate = Data.userModels.filter({$0.itemModels.contains(item)})
for i in 0..<Data.userModels.count {
if let idx = Data.userModels[i].itemModels.firstIndex(of: item) {
let oldPrice = Data.userModels[i].itemModels[idx].price
let newPrice = oldPrice * Double(duplicate.count+1)
Data.userModels[i].itemModels[idx].price = newPrice / Double(duplicate.count)
}
}
tableView.reloadData()
actionPerformed(true)
}
return UISwipeActionsConfiguration(actions: [delete])
}
Perhaps you can try this. Best of luck, please comment if it doesn't work. I am new to Swift :)
let actualItem = Data.userModels[indexPath.section].itemModels[indexPath.row]
for i in 0..<duplicate.count {
if let idx = duplicate[i].itemModels.firstIndex(of: actualItem) {
let oldPrice = duplicate[i].itemModels[idx].price
duplicate[i].itemModels[idx].price = oldPrice * duplicate.count
}
}
I have a user object that takes in a few fields such as name, email, and an occupation field that stores a string array.
When a user is creating an account, they're prompted to enter up to 3 occupational choices, which is then stored in an array and uploaded to firebase under their user id:
I have a table view controller with the purpose of displaying all of the users, but separated by their occupation. The idea is to allow users to search other people based off of their particular occupation so for reference, all the designers on the app can be found on the tableview controller under the section titled "Designer". In another scenario, if the user is a Designer AND a developer, they should show up under Developer as well.
The key part here is the fact that users enter their occupation manually. So I can just create an array with a specific set of occupational titles. I have to get a list of all occupations, and then filter the users based off of the occupational title. Using the help I received from a smaller version of this idea (Smaller Version), I came up with a function to separate users by their occupation but it's not working at all:
func getOccupations() -> [IndexedOccupations] {
while !users.isEmpty {
//-- filter the users
guard let referencedUser = users.first else { return [] }
for x in (referencedUser.occupation?.count)! {
let filteredUsers = users.filter { (user) -> Bool in
for y in user.occupation.count {
return user.occupation[y] == referencedUser.occupation[x]
}
return false
}
//-- create an occupation object with an occupation name, and list of filtered users & return occupations
let indexedOccupation = IndexedOccupations(occupation: referencedUser.occupation[x]!, users: filteredUsers)
occupations.append(indexedOccupation)
}}
return occupations
}
Heres how both objects look for reference btw:
class User: NSObject {
//-- things that a User will have stored
var id: String?
var bio: String?
var city: String?
var name: String?
var email: String?
var picture: String?
var state: String?
var links: [String]?
var playerID: String?
var occupation: [String]?
//-- initializer method
init(dictionary: [String: AnyObject]) {
self.id = dictionary["id"] as? String
self.name = dictionary["Name"] as? String
self.email = dictionary["Email"] as? String
self.city = dictionary["City"] as? String
self.state = dictionary["State"] as? String
self.picture = dictionary["Picture"] as? String
self.bio = dictionary["Bio"] as? String
self.playerID = dictionary["playerID"] as? String
self.links = dictionary["Links"] as? [String]
self.occupation = dictionary["Occupation"] as? [String]
}
}
And Occupations:
class IndexedOccupations: NSObject {
//-- things that a user will have stored
var occupation: String?
var users: [User]
//-- initializer method
init(occupation: String, users: [User]) {
self.occupation = occupation
self.users = users
}
}
I'd make a dictonary like:
var userByOccupation: [String: [User]] = []
And then make a function where you insert all Users in this Dictonary with the Key being their Occupation (if they have more then 1 just add new Key with same Value)
So I don't know how your Data Model looks like until Occupations but what you need to do is just iterate over every child of Occupations and for every child take the value and save this value as key and the current User as Value into the userByOccupation Array.
So first get the current User and then:
ref?.child("Occupation").observeSingleEvent(of: .value, with: { (snapshot) in
let enumerator = snapshot.children
while let rest = enumerator.nextObject() as? DataSnapshot{
if let occupation = rest.key as? String{
//And here insert OCCUPATION/USER like
if (userByOccupation[occupation] != nil) {
userByOccupation[occupation]?.append(user)
} else {
userByOccupation[occupation] = [user]
}
}
}
})
EDIT to comment-question:
I don't know if it is the best solution but here's how I'd do it.
After loading the dictionary just make an array of all keys like this
allOccupations = Array(userByOccupation.keys)
After this you can order this array for example -> dictionaries are unordered!
And then:
override func numberOfSections(in tableView: UITableView) -> Int {
return allOccupations.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return allOccupations[section]
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return userByOccupation[allOccupations[section]].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Then get the user like this
guard let allUsers = userByOccupation[allOccupations[section]] else {
return }
let currentUser = allUsers[indexPath.row]
}
Made this pretty fast so I'm sorry if there are errors in it, I hope it helps you!
I'm working in XCode 9.3, Swift developing an application for MacOS.
I've created a table that currently has two columns. I want to use type select that only acts on one column so that when the user types the first few letters, it only selects within the first column and not in the 2nd column.
The Apple documentation has an example in Objective-C here: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/RowSelection/RowSelection.html#//apple_ref/doc/uid/10000026i-CH6-SW1
but I can't seem to translate that into Swift. I've included the table code below. Any help at all would be greatly appreciated.
extension ViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return directoryItems?.count ?? 0
}
// Sorting.
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
guard let sortDescriptor = tableView.sortDescriptors.first else {
return
}
if let order = Directory.FileOrder(rawValue: sortDescriptor.key!) {
sortOrder = order
sortAscending = sortDescriptor.ascending
reloadFileList()
}
}
}
extension ViewController: NSTableViewDelegate {
fileprivate enum CellIdentifiers {
static let NameCell = "NameCellID"
static let TimeCell = "TimeCellID"
static let SizeCell = "SizeCellID"
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
var image: NSImage?
var text: String = ""
var cellIdentifier: String = ""
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .long
guard let item = directoryItems?[row] else {
return nil
}
if tableColumn == tableView.tableColumns[0] {
image = item.icon
text = item.name
cellIdentifier = CellIdentifiers.NameCell
} else if tableColumn == tableView.tableColumns[1] {
let asset = AVAsset(url: item.url as URL)
let audioTime = asset.duration
text = audioTime.durationText
cellIdentifier = CellIdentifiers.TimeCell
} else if tableColumn == tableView.tableColumns[2] {
text = item.isFolder ? "--" : sizeFormatter.string(fromByteCount: item.size)
cellIdentifier = CellIdentifiers.SizeCell
}
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: nil) as? NSTableCellView {
cell.textField?.stringValue = text
cell.imageView?.image = image ?? nil
return cell
}
return nil
}
}
Your problem is twofold:
First, you need to set an Identifier for each of your table columns. You can do this by selecting the column in Interface Builder, going to the Identity Inspector, and setting it to some string value:
Once you've done that, create some static properties to refer to these identifiers (this isn't strictly necessary, since you could of course just use rawValue to do a plain string comparison the Objective-C way, but the new Swift 4 identifiers are type-safe, and thus preferred). Do that like this:
extension ViewController: NSTableViewDelegate {
private struct TableColumns {
static let foo = NSUserInterfaceItemIdentifier("Foo")
static let bar = NSUserInterfaceItemIdentifier("Bar")
}
...
}
Now, you can use these identifiers to refer to your columns, using tableColumn(withIdentifier:) (or column(withIdentifier:) if you just want the column number). I recommend doing this everywhere you refer to them, including in your tableView(:viewFor:row:) method, since the way you're doing it now with tableView.tableColumns[0] and so forth depends on the order of the columns, and if the user reorders them, it may cause unexpected behavior. Using the identifier will make sure you're always looking at the column you think you're looking at.
Anyway, once you've got your identifiers set up, you can address the second problem: You're using the wrong delegate method. Instead of tableView(:shouldTypeSelectFor:searchString:), which is meant for catching these things at the event level (i.e., the user just pressed a key. Should I invoke the type select system at all?), use tableView(:typeSelectStringFor:row:). This method lets you return the string given to the type selection mechanism for each row/column combination in your table. For the ones you want type-select to ignore, just return nil; otherwise, return the string that you'd expect to have to type to select that particular row. So, something like:
func tableView(_ tableView: NSTableView, typeSelectStringFor tableColumn: NSTableColumn?, row: Int) -> String? {
if tableColumn?.identifier == TableColumns.foo {
return directoryItems?[row].name
} else {
return nil
}
}
I'd like to search through items of my plist. The plist consists of an array of dictionaries. Each key/value represents Strings/Ints, etc but that isn't important.
As you'll see in the tableViewController class below, I've currently got an array that I have typed. I know I need to make an array of objects/items from my plist but I can't work out how to reference objects from the plist in the view controller.
View controller.swift file:
import UIKit
class TableViewController: UITableViewController, UISearchResultsUpdating {
var array = ["Example 1", "Example 2", "Example 3"]
var filteredArray = [String]()
var searchController = UISearchController()
var resultsController = UITableViewController()
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: resultsController)
tableView.tableHeaderView = searchController.searchBar
searchController.searchResultsUpdater = self
resultsController.tableView.delegate = self
resultsController.tableView.dataSource = self
}
//Added func to update search results
func updateSearchResults(for searchController: UISearchController) {
filteredArray = array.filter({ (array:String) -> Bool in
if array.contains(searchController.searchBar.text!) {
return true
} else {
return false
}
})
resultsController.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension TableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == resultsController.tableView {
return filteredArray.count
} else {
return array.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
if tableView == resultsController.tableView {
cell.textLabel?.text = filteredArray[indexPath.row]
} else {
cell.textLabel?.text = array[indexPath.row]
}
return cell
}
}
I've tried solving this by creating an object class from a tutorial on plists. It uses the example of a periodic table of elements:
import UIKit
struct Element {
enum State: String {
case Solid, Liquid, Gas
}
let atomicNumber: Int
let atomicWeight: Float
let discoveryYear: String
let group: Int
let name: String
let period: Int
let radioactive: Bool
let state: State
let symbol: String
// Position in the table
let horizPos: Int
let vertPos: Int
}
extension Element {
enum ErrorType: Error {
case noPlistFile
case cannotReadFile
}
/// Load all the elements from the plist file
static func loadFromPlist() throws -> [Element] {
// First we need to find the plist
guard let file = Bundle.main.path(forResource: "Element", ofType: "plist") else {
throw ErrorType.noPlistFile
}
// Then we read it as an array of dict
guard let array = NSArray(contentsOfFile: file) as? [[String: AnyObject]] else {
throw ErrorType.cannotReadFile
}
// Initialize the array
var elements: [Element] = []
// For each dictionary
for dict in array {
// We implement the element
let element = Element.from(dict: dict)
// And add it to the array
elements.append(element)
}
// Return all elements
return elements
}
/// Create an element corresponding to the given dict
static func from(dict: [String: AnyObject]) -> Element {
let atomicNumber = dict["atomicNumber"] as! Int
let atomicWeight = Float(dict["atomicWeight"] as! String) ?? 0
let discoveryYear = dict["discoveryYear"] as! String
let group = dict["group"] as! Int
let name = dict["name"] as! String
let period = dict["period"] as! Int
let radioactive = dict["radioactive"] as! String == "True"
let state = State(rawValue: dict["state"] as! String)!
let symbol = dict["symbol"] as! String
let horizPos = dict["horizPos"] as! Int
let vertPos = dict["vertPos"] as! Int
return Element(atomicNumber: atomicNumber,
atomicWeight: atomicWeight,
discoveryYear: discoveryYear,
group: group,
name: name,
period: period,
radioactive: radioactive,
state: state,
symbol: symbol,
horizPos: horizPos,
vertPos: vertPos)
}
}
And in the viewController class, instead of having
var array = ["Example 1", "Example 2", "Example 3"]
I've tried variations of
var array = Element["name"]
and
var array = elements.name
But they obviously don't work because the reference to the plist is in the object class.
If anyone has any idea on how to solve this using swift 3/xcode 8 I would be very appreciative!!
I hope your question is still relevant. As I understand, you can't filter your array, right? If so, I recommend you to take a look at this and this tutorials. Both of them are representing a little bit different approaches to load filtered array, but it doesn't matter much, they work.
P.S. I don't recommend you to make a special tableView for searching, if you want to customize it hereafter, because you will have to do it programmaticly later.
It will be more efficiant to do like this:
If searchController.isActive {
// do some stuff
} else { // another stuff }
But it is just my opinion. I hope my question will help ;).
Thanks for the links Oleg. They were really good!!
It wasn't so much the filtering I had a problem with but actually parsing objects from my plist into a tableview. It took me a few days but I found an answer in case other people were also having the same problem. Keep in mind I'm reasonably new to this and so it might not be the best/perfect way of doing it but it works.
In viewDidLoad, I made a reference to the plist using the following code:
let path = Bundle.main.path(forResource: "Elements", ofType: "plist")
let dictArray = NSArray(contentsOfFile: path!)
I'm not sure of the relevance but I'm pretty sure this next bit is needed if the plist ever needed to be updated. (Also in viewDidLoad)
for elementItem in dictArray! {
let newElement : Element = Element(state:((elementItem as AnyObject).object(forKey: "state")) as! String,
atomicNumber:((elementItem as AnyObject).object(forKey: "atomicNumber")) as! Int,
atomicWeight:((elementItem as AnyObject).object(forKey: "atomicWeight")) as! Float,
discoveryYear:((elementItem as AnyObject).object(forKey: "discoveryYear")) as! String
etc. for each of the keys in the dictionary in the same order as in the Element object in the question. Then (also in viewDidLoad):
elementsArray.append(newElement)
Then its pretty easy, in cellForRow I just have to make a new variable that refers back to the object and I can call up each/any of the associated dictionary keys. E.g.:
let element : Element
cell.textLabel!.text = element.name
cell.detailTextLabel?.text = element.symbol
return cell
Like I said, I know its not perfect but it worked for me. I've heard its not best practice to put too much information in viewDidLoad, so someone else might be able to confirm or provide a better answer.