I'm writing a program which have array of 40k (40000) items, and present it in UITableView, after search table should filter and present only search results.
The problem is that deleting of many rows at once (for example 30000+) takes about 10 - 20 seconds and sure it's not possible to use. Can you suggest any decision of this problem?
(tableview.reloadData() not suited)
var allProducts = [Product]()
#IBOutlet weak var searchTableView: UITableView!
#IBOutlet weak var searchTextField: UITextField?
var searchResults = [Product]()
enum Action{
case Insert
case Ignore
case Remove
}
override func viewDidLoad() {
super.viewDidLoad()
searchTextField?.addTarget(self, action: #selector(ViewController.textFieldDidChanged(_:)), for: .editingChanged)
DBBrain().getAllAlcProducts() { [weak self] products in
self?.allProducts = products
}
}
func textFieldDidChanged(_ sender: UITextField){
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let text = sender.text!.lowercased()
let res = self!.allProducts.filter({ $0.name.lowercased().contains(text) })
if self?.searchTextField?.text != nil && text == self!.searchTextField!.text!{
if let values = self?.getIndexes(forResults: res){
self?.searchResults = res
self?.updateTable(action: values.0, indexes: values.1)
}
}
}
}
private func getIndexes(forResults products: [Product]) -> (Action, [IndexPath]){
var indexes = [IndexPath]()
var action = Action.Ignore
if searchResults.count > products.count{
var newCounter = 0
for x in 0..<searchResults.count {
if products.isEmpty || searchResults[x].id != products[newCounter].id {
indexes.append(IndexPath(row: x, section: 0))
}else {
if newCounter < products.count - 1{
newCounter += 1
}
}
}
action = Action.Remove
}else if searchResults.count < products.count{
var oldCounter = 0
for x in 0..<products.count {
if searchResults.isEmpty || searchResults[oldCounter].id != products[x].id {
indexes.append(IndexPath(row: x, section: 0))
}else {
if oldCounter < searchResults.count - 1 {
oldCounter += 1
}
}
}
action = Action.Insert
}
return (action, indexes)
}
private func updateTable(action: Action, indexes: [IndexPath]) {
DispatchQueue.main.async { [weak self] in
if action != .Ignore {
if action == .Remove {
self?.searchTableView.beginUpdates()
self?.searchTableView.deleteRows(at: indexes, with: .fade)
self?.searchTableView.endUpdates()
}else if action == .Insert {
self?.searchTableView.beginUpdates()
self?.searchTableView.insertRows(at: indexes, with: .fade)
self?.searchTableView.endUpdates()
}
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchResults.count
}
You should never have a table with 30k+ cells. That's a ridiculous usage of memory. You should set a max limit to the number of cells in your rowsForSection delegate method.
Get the count of your results, and if the count is greater than 30 or so (even that is a large number), set it to 30.
It may very well be having a super huge array, too. That's a lot of memory usage, even if you aren't making cells for those.
Related
When to try open new controller with my tableview and progress bar I have "Index out of range".
I fetch data from Firestore Database and the idea is that when I tap button "next" I want to update progress bar and reload table view with new values. Number of rows must be equal number of sets, but each exercise has different amount of sets, so I want to change number of rows dynamically.
class StartViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var titleValue: String = ""
var dayOfWorkout: String = ""
var exerciseNumber = 0
var models: [DataCell2] = []
let user = Auth.auth().currentUser
var db = Firestore.firestore()
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var exerciseLabel: UILabel!
#IBOutlet weak var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
progressView.transform = progressView.transform.scaledBy(x: 1, y: 3)
tableView.delegate = self
tableView.dataSource = self
retrieveWorkouts()
}
#IBAction func nextButtonPressed(_ sender: UIButton) {
nextExercise()
Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateUI), userInfo: nil, repeats: false)
tableView.reloadData()
}
#objc func updateUI() {
progressView.progress = getProgress()
}
func getProgress() -> Float {
return Float(exerciseNumber) / Float(models.count)
}
func nextExercise() {
if exerciseNumber + 1 < models.count {
exerciseNumber += 1
} else {
exerciseNumber = 0
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
**//HERE IS ERROR**
return models[exerciseNumber].sets!
}
And methods to fetch data from firestore
func retrieveWorkouts() {
db.collection("users").document("\(user!.uid)").collection("WorkoutsName").document("\(titleValue)").collection("Exercises").order(by: "Number").getDocuments { (querySnapshot, error) in
if let error = error{
print("\(error.localizedDescription)")
}else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
if let numberDb = data["Number"] as? Int, let exerciseDb = data["Exercise"] as? String, let kgDb = data["kg"] as? Int, let setsDb = data["Sets"] as? Int, let repsDb = data["Reps"] as? Int, let workoutName = data["workoutName"] as? String {
print("data is \(data)")
let newModel = DataCell2(Number: numberDb, Exercise: exerciseDb, kg: kgDb, sets: setsDb, reps: repsDb, workoutName: workoutName)
self.models.append(newModel)
self.tableView.reloadData()
}
}
}
}
}
}
You have a timing issue. The view will try to create the tableView when it loads. At that point the models array will be empty as the async firebase method won't have run and updated it. But you initialise exerciseNumber to 0.
This means that the numberOfRowsInSection delegate method will try to access models[0] but as the array is empty at this point this doesn't exist and causes the index out of range crash.
You can fix this by a bit of defensive programming:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard !model.isEmpty else {return 0}
return models[exerciseNumber].sets!
}
You could also add a second guard or a precondition to ensure that exerciseNumber < models.count. While from the bit of code you have posted this should not be necessary, it's never a bad idea to programme defensively.
You are basing the number of rows on
models[exerciseNumber].sets!
and you are setting the exercise number in
func nextExercise() {
if exerciseNumber + 1 < models.count {
exerciseNumber += 1
} else {
exerciseNumber = 0
}
}
However you have a logic error in here: you are checking the exercise number is less than the models.count, and if so increasing it. This will lead to exerciseNumber == models.count, and as arrays are zero-indexed this will be beyond the end of the array and give the out-of-range error.
An easy fix would be to change the method to:
func nextExercise() {
exerciseNumber += 1
if exerciseNumber == models.count {
exerciseNumber = 0
}
return exerciseNumber
}
There are smarter ways of doing this, but this will work for now.
I have structured my app using the MVVM pattern, the datasource of the collectionView gets data from the viewModel.
In viewModel I have a closure which gets called after updating the data, it passes IndexSet of sections and [IndexPath] of items which collectionView should insert. However I get the crash everytime after calling the insert method with error:
'Invalid update: invalid number of sections. The number of sections contained in the collection view after the update (11) must be equal to the number of sections contained in the collection view before the update (11), plus or minus the number of sections inserted or deleted (11 inserted, 0 deleted).'
I understand what this error means, however I noticed that this is the order in which the methods are called
1. viewDidLoad()
2. numberOfSections(in collectionView: UICollectionView) // returns 0
3. update?(insertedSectionsSet, insertedRows) // the closure gets called in viewModel, insertedSectionsSet has 11 elements, that's correct
4. numberOfSections(in collectionView: UICollectionView) // returns 11, that's correct but it should be called after next point
5. viewModel.update = { [weak self] sections, items in
DispatchQueue.main.async {
self?.collectionView.performBatchUpdates({
self?.collectionView.insertSections(sections) // crash
self?.collectionView.insertItems(at: items)
}, completion: nil)
}
}
As you can see in the code below, I added prints before calling each methods and you can clearly see that numberOfSections gets called after calling the closure and before performing it. It makes absolutely no sense to me why is it happening. I believe that the cause of crash lies in calling the numberOfSections before inserting them, because then it expects 22 sections after inserting.
update
numberofsections
closure
numberofsections
Code:
class PlaylistsDataSource: NSObject, UICollectionViewDataSource {
...
func numberOfSections(in collectionView: UICollectionView) -> Int {
print("numberofsections")
return viewModel.numberOfSections
}
...
}
class PlaylistsMasterViewController: UIViewController {
...
viewDidLoad() {
viewModel.update = { [weak self] sections, items in
DispatchQueue.main.async {
print("closure")
self?.collectionView.performBatchUpdates({
self?.collectionView.insertSections(sections)
self?.collectionView.insertItems(at: items)
}, completion: nil)
}
}
}
...
}
class PlaylistsMasterViewModel {
private var sectionIndexes = [String]()
private var playlistsDictionary = [String: [AWPlaylist]]()
var update: ((IndexSet, [IndexPath]) -> Void)?
var numberOfSections: Int {
return sectionIndexes.count
}
EDIT: Added more code
extension PlaylistsMasterViewModel {
func fetchPlaylists() {
repo.getAllPlaylists(from: [.iTunes]) { [weak self] result in
switch result {
case .success(let playlists):
self?.sortIncoming(playlists: playlists)
case .failure(let error):
print(error.localizedDescription)
}
}
}
private func sortIncoming(playlists: [AWPlaylist]) {
var insertedPlaylists = [(key: String, list: AWPlaylist)]()
var insertedIndexes = [String]()
func insertNewSection(playlist: AWPlaylist, key: String) {
insertedIndexes.append(key)
playlistsDictionary.updateValue([playlist], forKey: key)
}
func insertNewRow(playlist: AWPlaylist, key: String) {
guard var value = playlistsDictionary[key] else {
print("Oh shit")
return
}
value.append(playlist)
value.sort(by: SortingPredicates.Playlist.nameAscending)
playlistsDictionary.updateValue(value, forKey: key)
insertedPlaylists.append((key, playlist))
}
for list in playlists {
let name = list.localizedName.uppercased().trimmingCharacters(in: CharacterSet.whitespaces)
guard let firstCharacter = name.first else { return }
let firstLetter = String(firstCharacter)
let key: String
if CharacterSet.english.contains(firstLetter.unicodeScalars.first!) {
key = firstLetter
} else if CharacterSet.numbers.contains(firstLetter.unicodeScalars.first!) {
key = "#"
} else {
key = "?"
}
if playlistsDictionary[key] == nil {
insertNewSection(playlist: list, key: key)
} else {
insertNewRow(playlist: list, key: key)
}
}
sectionIndexes.append(contentsOf: insertedIndexes)
sectionIndexes.sort(by: { $0 < $1 })
let insertedSections = insertedIndexes.compactMap { index -> Int? in
guard let sectionIndex = self.sectionIndexes.firstIndex(of: index) else {
return nil
}
return sectionIndex
}
let insertedSectionsSet = IndexSet(insertedSections)
let insertedRows = insertedPlaylists.compactMap { tuple -> IndexPath? in
if let section = self.sectionIndexes.firstIndex(of: tuple.key) {
if let row = self.playlistsDictionary[tuple.key]?.firstIndex(where: { $0.equals(tuple.list) }) {
return IndexPath(row: row, section: section)
}
}
return nil
}
print("update")
update?(insertedSectionsSet, insertedRows)
}
I have a simple tableView with saved data. I created a delete button that lets me multi-delete from realm. That part works, it is when the tableview is suppose to reload that it seems to not work. I have seen a lot of answers that say you should reload it on the main thread, or view or whatever, using dispatchQueue.main.async
using just normal tableView.reloadData() didn't reload the tableview but when I use the dispatchQueue version it does delete a value but usually the last value in the tableView.
For example my tableView has the strings Uno and Un in that descending order. If I chose to delete Uno when I press the delete button the tableview does reload leaving only one value but that value is Uno, but realm Database tells me I deleted Uno and when I go back to that view it shows Un. It just isn't reloading correctly.
I have tried to place the reloadData in the dispatch at many different locations, but it still doesn't reload correctly. I am curious what I am doing wrong.
this is the viewController with the tableview where I delete the data in the tableView:
import UIKit
import Realm
import RealmSwift
class OtherViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var otherTableView: UITableView!
var realm: Realm!
var realmedData = ""
var realmList: Results<Realmed> {
get {
return realm.objects(Realmed.self)
}
}
let deleteBtn = UIBarButtonItem()
var testingBool = false
var realmArr = [String]()
var idValue = [Int]()
var idArr = [Int]()
var spanArrValue: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
otherTableView.reloadData()
realm = try! Realm()
self.otherTableView.delegate = self
self.otherTableView.dataSource = self
self.otherTableView.reloadData()
deleteBtnInfo(btn: deleteBtn)
self.navigationItem.rightBarButtonItem = deleteBtn
}
func deleteBtnInfo(btn: UIBarButtonItem) {
btn.title = "Delete"
btn.style = .plain
btn.target = self
btn.action = #selector(didTapDeleteBtn(sender:))
testingBool = false
}
#objc func didTapDeleteBtn(sender: AnyObject) {
testingBool = !testingBool
if testingBool == true {
deleteBtn.title = "Remove"
otherTableView.allowsMultipleSelection = true
otherTableView.allowsMultipleSelectionDuringEditing = true
} else if testingBool == false {
deleteBtn.title = "Delete"
didPressRemove()
DispatchQueue.main.async {
self.otherTableView.reloadData()
}
otherTableView.allowsMultipleSelection = false
otherTableView.allowsMultipleSelectionDuringEditing = false
}
}
func didPressRemove() {
if idValue.count == 0 {
print("Please Select what to Delete")
} else {
deleteRealm(idInt: idValue)
}
}
func deleteRealm(idInt: [Int]) {
do {
try realm.write {
for deleteIndex in idInt {
let deleteValue = realm.objects(RealmTwo.self).filter("id == %#", deleteIndex as Any)
print(deleteIndex)
realm.delete(deleteValue)
}
}
} catch {
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var counted = realm.objects(RealmTwo.self).filter("realmLbl == %#", realmedData)
return counted.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "otherCell", for: indexPath) as! OtherTableViewCell
var celledItem = realm.objects(Realmed.self)
for item in celledItem {
for items in item.realmTwo {
self.idArr.append(items.id)
self.realmArr.append(items.spanish)
}
}
cell.otherLbl.text = "\(realmArr[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if testingBool == false {
print(realmArr[indexPath.row])
} else {
self.idValue.append(idArr[indexPath.row])
print(spanArrValue)
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if testingBool == true {
if let index = idValue.index(of: idArr[indexPath.row]) {
idValue.remove(at: index)
print(spanArrValue)
}
}
}
}
this is the realm class for the data that I am trying to delete.
import Foundation
import UIKit
import Realm
import RealmSwift
class RealmTwo: Object {
#objc dynamic var id = Int()
#objc dynamic var realmLbl = String()
#objc dynamic var spanish = String()
#objc dynamic var french = String()
let realmed = LinkingObjects(fromType: Realmed.self, property: "realmTwo")
convenience init(id: Int, realmLbl: String, spanish: String, french: String) {
self.init()
self.id = id
self.realmLbl = realmLbl
self.spanish = spanish
self.french = french
}
}
As I said above, I placed reloadData() in different places and these are where I placed them, just in case you want to know:
func didPressRemove() {
if idValue.count == 0 {
print("Please Select what to Delete")
} else {
deleteRealm(idInt: idValue)
DispatchQueue.main.async {
self.otherTableView.reloadData()
}
}
}
func deleteRealm(idInt: [Int]) {
do {
try realm.write {
for deleteIndex in idInt {
let deleteValue = realm.objects(RealmTwo.self).filter("id == %#", deleteIndex as Any)
print(deleteIndex)
realm.delete(deleteValue)
DispatchQueue.main.async {
self.otherTableView.reloadData()
}
}
}
} catch {
}
}
I am just not sure where the reloadData is suppose to go, or if that is the real problem. Thank you for the help, and ask if there is anything else I can do.
There are a couple of issues but the main issue is that you're deleting the object from realm but that object is still hanging around in your dataSource tableView array, realmArr.
There are a whole bunch of solutions but the simplest is to add an observer to the realm results and when an item is added, changed or removed, have that update your dataSource array and then reload the tableview. One option also here is to use those results as the dataSource instead of a separate array. Realm Results objects behave very similar to an array and are great a a dataSource.
Conceptually the realm code is similar to
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
tableView.reloadData() //this is when the realm data is intially loaded.
case .update(_, let deletions, let insertions, let modifications):
//handle add, edit and modify per event.
// with an add, add the provided object to your dataSource
// same thing for remove and modify
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
//reload the tableView now the dataSource has been updated
}
There are several options of handling those events and they are all covered in the Realm documentation. See Realm Notifications for further details about setting up the notifications.
A second option is to manually keep things in sync; e.g. when deleting the item from Realm, also delete the item from your dataSource array
This is how I managed to solve this problem.
import UIKit
import Realm
import RealmSwift
class OtherViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var notificationToken: NotificationToken? = nil
#IBOutlet weak var otherTableView: UITableView!
var realm: Realm!
var realmedData = ""
var realmList: Results<RealmTwo> {
get {
return realm.objects(RealmTwo.self).filter("%# == realmLbl", realmedData)
}
}
var realmingList: Results<RealmTwo> {
get {
return realm.objects(RealmTwo.self)
}
}
let deleteBtn = UIBarButtonItem()
var testingBool = false
var realmArr = [String]()
var idValue = [Int]()
var idArr = [Int]()
var spanArrValue: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
otherTableView.allowsMultipleSelectionDuringEditing = true
realm = try! Realm()
notificationToken = realmList.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.otherTableView else {return}
switch changes {
case .initial:
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
fatalError("\(error)")
}
}
self.otherTableView.delegate = self
self.otherTableView.dataSource = self
self.otherTableView.reloadData()
deleteBtnInfo(btn: deleteBtn)
self.navigationItem.rightBarButtonItem = deleteBtn
}
func deleteBtnInfo(btn: UIBarButtonItem) {
btn.title = "Delete"
btn.style = .plain
btn.target = self
btn.action = #selector(didTapDeleteBtn(sender:))
testingBool = false
}
#objc func didTapDeleteBtn(sender: AnyObject) {
testingBool = !testingBool
if testingBool == true {
deleteBtn.title = "Remove"
} else if testingBool == false {
deleteBtn.title = "Delete"
}
}
func didPressRemove() {
if testingBool == false {
print("Select what to Delete")
} else {
deleteRealm(idInt: idValue)
otherTableView.isEditing = false
}
}
#IBAction func pressEdit(_ sender: Any) {
testingBool = !testingBool
if testingBool == true {
otherTableView.isEditing = true
} else if testingBool == false {
otherTableView.isEditing = false
}
}
#IBAction func pressDelete(_ sender: Any) {
deleteRealm(idInt: idValue)
}
func deleteRealm(idInt: [Int]) {
do {
try realm.write {
for deleteIndex in idInt {
let deletingValue = realmList.filter("id == %#", deleteIndex as Any)
print("DeleteValue: \(deletingValue)")
realm.delete(deletingValue)
}
}
} catch {
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return realmList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "otherCell", for: indexPath) as! OtherTableViewCell
cell.otherLbl.text = realmList.filter("%# == realmLbl", realmedData)[indexPath.row].spanish
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if otherTableView.isEditing == false {
} else {
let idArr = realmList.filter("%# == realmLbl", realmedData)[indexPath.row].id
self.idValue.append(idArr)
print("ID: \(idValue)")
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if otherTableView.isEditing == true {
let idArr = realmList.filter("%# == realmLbl", realmedData)[indexPath.row].id
if let index = idValue.index(of: idArr) {
idValue.remove(at: index)
print("ID: \(idValue)")
}
}
}
deinit {
notificationToken?.invalidate()
}
}
Thank you
Not looking for code (wouldn't hurt though) just looking to know how I should go about this. I have an app which is like a blog reader. I have information in a MYSQL database which is taken with JSON, placed in a jsonArray and then placed in an array to be shown in a table view. In that table view I generally have all my objects in 1 section. Each row/object has a button when clicked it moves that row into another section. I also have a search controller to search through the main section (Section 1).
How do I save the order or position of the rows to Core Data?
For example: I have 0 rows in Section 0 and 5 rows in Section 1, I click the button on one of the rows in Section 1 and it moves that row to Section 0, now Section 0 has 1 row while Section 1 has 4 rows. How do I save this new tableview order to Core Data? I just want to save the rows positions, so the app remembers which section that selected row was in.
Do I save the indexPath of the row in the section?
When adding an entity and attributes, what do I use to save?
Also, its a mysql reader so when the tableview is reloaded and a new content is added, will it still show since the tableview will be reading from core data?
I'm learning Core Data (Swift 3 code) but just having trouble using it for this app.
Thank you for helping out!
Use UserDefaults to save the ordering data if you don't need to save the whole data from MySQL database. Define a class only contains dataId and indexPath:
class DataOrdering: NSObject, NSCoding {
var indexPath: IndexPath?
var dataId: String?
init(dataId: String, indexPath: IndexPath) {
super.init()
self.dataId = dataId
self.indexPath = indexPath
}
required init(coder aDecoder: NSCoder) {
if let dataId = aDecoder.decodeObject(forKey: "dataId") as? String {
self.dataId = dataId
}
if let indexPath = aDecoder.decodeObject(forKey: "indexPath") as? IndexPath {
self.indexPath = indexPath
}
}
func encode(with aCoder: NSCoder) {
aCoder.encode(dataId, forKey: "dataId")
aCoder.encode(indexPath, forKey: "indexPath")
}
func save(defaults key: String) -> Bool {
let defaults = UserDefaults.standard
let savedData = NSKeyedArchiver.archivedData(withRootObject: self)
defaults.set(savedData, forKey: key)
return defaults.synchronize()
}
convenience init?(defaults key: String) {
let defaults = UserDefaults.standard
if let data = defaults.object(forKey: key) as? Data,
let obj = NSKeyedUnarchiver.unarchiveObject(with: data) as? DataOrdering,
let dataId = obj.dataId,
let indexPath = obj.indexPath {
self.init(dataId: dataId, indexPath: indexPath)
} else {
return nil
}
}
class func allSavedOrdering(_ maxRows: Int) -> [Int: [DataOrdering]] {
var result: [Int: [DataOrdering]] = [:]
for section in 0...1 {
var rows: [DataOrdering] = []
for row in 0..<maxRows {
let indexPath = IndexPath(row: row, section: section)
if let ordering = DataOrdering(defaults: indexPath.defaultsKey) {
rows.append(ordering)
}
rows.sort(by: { $0.indexPath! < $1.indexPath! })
}
result[section] = rows
}
return result
}
}
Playground sample:
let data = DataOrdering(dataId: "1", indexPath: IndexPath(row: 0, section: 0))
let savedData = NSKeyedArchiver.archivedData(withRootObject: data)
let obj = NSKeyedUnarchiver.unarchiveObject(with: savedData) as? DataOrdering
obj?.dataId // print: "1"
obj?.indexPath // print: [0,0]
Use save function to save with a "key" and read it back by DataOrdering(defaults: "key")
UPDATED
Added codes for a view controller to use this class:
extension IndexPath {
var defaultsKey: String { return "data_ordering_\(section)_\(row)" }
}
class ViewController: UITableViewController {
var data: [Any]?
var items: [[Any]]?
func fetchData() {
// request from remote or local
data = [1, 2, 3, "a", "b", "c"] // sample data
// Update the items to first section has 0 elements,
// and place all data in section 1
items = [[], data ?? []]
// apply ordering
applySorting() { "\($0)" }
// save ordering
saveSorting() { "\($0)" }
// refresh the table view
tableView.reloadData()
}
func applySorting(_ dataIdBlock: (Any) -> String) {
// get all saved ordering
guard let data = self.data else { return }
let ordering = DataOrdering.allSavedOrdering(data.count)
var result: [[Any]] = [[], []]
for (section, ordering) in ordering {
guard section <= 1 else { continue } // make sure the section is 0 or 1
let rows = data.filter({ obj -> Bool in
return ordering.index(where: { $0.dataId == .some(dataIdBlock(obj)) }) != nil
})
result[section] = rows
}
self.items = result
}
func saveSorting(_ dataIdBlock: (Any) -> String) {
guard let items = self.items else { return }
for (section, rows) in items.enumerated() {
for (row, item) in rows.enumerated() {
let indexPath = IndexPath(row: row, section: section)
let dataId = dataIdBlock(item)
let ordering = DataOrdering(dataId: dataId, indexPath: indexPath)
ordering.save(defaults: indexPath.defaultsKey)
}
}
}
#IBAction func buttonMoveToSectionTapped(_ sender: UIButton) {
// if the sender's tag is row index
// or you can get indexPath by tableView.indexPath(for: cell) function too
let row = sender.tag
// move this item from section 1 to section 0 (last position)
if let item = items?[1].remove(at: row) {
items?[0].append(item)
}
// Save all sorting
saveSorting() { "\($0)" }
tableView.reloadData() // refresh table view
}
override func numberOfSections(in tableView: UITableView) -> Int {
return self.items?.count ?? 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items?[section].count ?? 0
}
}
I have a tableview that I fill with results from an http call. The data is passed on into the array called tripTimes. triptimes contains information about certain trips made during a given day. The starting time of every trip of that day is passed into self.tableData and is inserted into the tableView.
After this, I reload the table and... strange results.
The array tableData gets filled properly, but after I call self.tableView.reloadData() almost all cells are properly filled. However, the first cell is left empty until I tap it or if I wait for about 5 seconds and the last cell is also empty, but it gets the same value as the first cell after the same time.
I think it has to do something with the dispatch_async part, but I'm fairly new to swift and iOS, so I have no clue where to look. When I print the contents of self.tableData inside the dispatch_async function, the array contains the proper values.
Below is the code for my viewController
import UIKit
import Foundation
import SwiftHTTP
class TripSelect: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var dateTextField: UITextField!
#IBOutlet weak var tableView: UITableView!
#IBAction func showTrips(sender: AnyObject) {
//if data is already present in tableData, empty it first
if self.tripTimes.count != 0 && self.tableData.count != 0 {
for var i=0; i<self.tripTimes.count; i++ {
self.tableData.removeObjectAtIndex(0)
let indexPathToRemove = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.deleteRowsAtIndexPaths([indexPathToRemove], withRowAnimation: .Automatic)
}
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
var request = HTTPTask()
request.GET("<MY_URL>", parameters: ["date": dateTextField.text], success: {(response: HTTPResponse) in
if let data = response.responseObject as? NSData {
let str = NSString(data: data, encoding: NSUTF8StringEncoding) as String
let str2 = "\"not enough data for this day\""
if str != str2 {
let json:AnyObject = JSON.parse(str)
self.dayLatLongs = json["dayLatLongs"] as Array
self.tripTimes = json["tripTimes"] as Array
var tripLatLongs:[AnyObject] = self.dayLatLongs[0] as Array
var firstTrip:[AnyObject] = tripLatLongs[0] as Array
var tripStartTime:String = ""
for var i=0; i<self.tripTimes.count; i++ {
tripStartTime = self.tripTimes[i]["startTime"] as String
//println("\(tripStartTime)")
self.tableData.addObject(tripStartTime)
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
dispatch_async(dispatch_get_main_queue(), {
println(self.tableData)
self.tableView.reloadData()
})
} else {println(str)}
}
},failure: {(error: NSError, response: HTTPResponse?) in
println("error: \(error)")
})
}
var dayLatLongs:[AnyObject] = []
var tripTimes:[AnyObject] = []
var tableData: NSMutableArray! = NSMutableArray()
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
cell.textLabel?.text = self.tableData.objectAtIndex(indexPath.row) as? String
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
var backgroundView = UIView(frame: CGRectZero)
self.tableView.tableFooterView = backgroundView
self.tableView.backgroundColor = UIColor.clearColor()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
UPDATE:
var indexPaths:[NSIndexPath] = [NSIndexPath]()
for var i=0; i<self.tripTimes.count; i++ {
tripStartTime = self.tripTimes[i]["startTime"] as String
self.tableData.addObject(tripStartTime)
indexPaths.append(NSIndexPath(forRow: i, inSection: 0))
}
self.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
Now the results are shown properly, but only after I 'drag' the table a couple of times.
Your problem is in the first if-clause:
if self.tripTimes.count != 0 && self.tableData.count != 0 {
for var i=0; i<self.tripTimes.count; i++ {
self.tableData.removeObjectAtIndex(0)
let indexPathToRemove = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.deleteRowsAtIndexPaths([indexPathToRemove], withRowAnimation: .Automatic)
}
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
You always remove just the value at index 0 and you don't use the i. So replace all hardcoded 0 with your index i:
if self.tripTimes.count != 0 && self.tableData.count != 0 {
for var i=0; i<self.tripTimes.count; i++ {
self.tableData.removeObjectAtIndex(i)
let indexPathToRemove = NSIndexPath(forRow: i, inSection: 0)
self.tableView.deleteRowsAtIndexPaths([indexPathToRemove], withRowAnimation: .Automatic)
}
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
All credit goes to #A-Live for this answer.
Updated with suggested changes:
To remove cells, I changed the function to
dispatch_async(dispatch_get_main_queue(), {
if self.tripTimes.count != 0 && self.tableData.count != 0 {
self.tableData = []
self.tableView.deleteRowsAtIndexPaths(self.indexPaths, withRowAnimation: .Automatic)
self.indexPaths = []
}
})
To add cells, I changed the function to
dispatch_async(dispatch_get_main_queue(), {
for var i=0; i<self.tripTimes.count; i++ {
tripStartTime = self.tripTimes[i]["startTime"] as String
self.tableData.addObject(tripStartTime)
self.indexPaths.append(NSIndexPath(forRow: i, inSection: 0))
}
self.tableView.insertRowsAtIndexPaths(self.indexPaths, withRowAnimation: .Automatic)
})