Guidance on Core Data with Swift 3 - ios

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
}
}

Related

Update that object with the new info in array and to display tableview Swift

I am using firebase realtime database and implementing user profile data with usersFriend and location. I need to implement the update in object array and show updated values in tableview. I have tried but I am not successful in updating object and then tableview reload. Function already developed.
I need to show updated object array swapped with new values and display in tableview.
var myFriendsDataSource = [FriendClass]()
func watchForChangesInMyFriends() {
let usersRef = self.ref.child("profiles") usersRef.observe(.childChanged, with: { snapshot in
let key = snapshot.key
if let friendIndex = self.myFriendsDataSource.firstIndex(where: { $0.uid == key} ) {
let friend = self.myFriendsDataSource[friendIndex]
print("found user \(friend.batteryStatus), updating")
self.myFriendsDataSource[friendIndex] = friend
self.tableView.reloadData()
}
})
}
Class:
class FriendClass {
var uid = ""
var name = ""
var batteryStatus = Int()
var latitude = Double()
var longitude = Double()
var timeStamp = Int64()
//var profilePic
init(withSnapshot: DataSnapshot) {
self.uid = withSnapshot.key
self.name = withSnapshot.childSnapshot(forPath: "name").value as? String ?? "No Name"
self.batteryStatus = withSnapshot.childSnapshot(forPath: "batteryStatus").value as? Int ?? 0
self.latitude = withSnapshot.childSnapshot(forPath: "latitude").value as? Double ?? 0.0
self.longitude = withSnapshot.childSnapshot(forPath: "longitude").value as? Double ?? 0.0
self.timeStamp = withSnapshot.childSnapshot(forPath: "timeStamp").value as? Int64 ?? 0
}
}
Updated:
func loadUsersFriends() {
let uid = "zzV6DQSXUyUkPHgENDbZ9EjXVBj2"
let myFriendsRef = self.ref.child("userFriends").child(uid)
myFriendsRef.observeSingleEvent(of: .value, with: { snapshot in
let uidArray = snapshot.children.allObjects as! [DataSnapshot]
for friendsUid in uidArray {
self.loadFriend(withUid: friendsUid.key)
print(friendsUid)
}
})
}
func loadFriend(withUid: String) {
let thisUserRef = self.ref.child("profiles").child(withUid)
thisUserRef.observeSingleEvent(of: .value, with: { snapshot in
let aFriend = FriendClass(withSnapshot: snapshot)
self.myFriendsDataSource.append(aFriend)
print(self.myFriendsDataSource)
self.tableView.reloadData()
self.watchForChangesInMyFriends()
})
}
Update 2:
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 70
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 10
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myFriendsDataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FriendListTableViewCell", for: indexPath) as! FriendListTableViewCell
let dic = myFriendsDataSource[indexPath.row]
cell.frndName.text = dic.name
return cell
}
Given the above comment discussion, I think you need to update your watchForChangesInMyFriends method as below to actually update the datasource with the new friend data. You should also do all your UI updates on the main thread, and as there is no guarantee that this closure will run on the main thread you need to force the tableView update onto the main thread.
func watchForChangesInMyFriends() {
let usersRef = self.ref.child("profiles") usersRef.observe(.childChanged, with: { snapshot in
let key = snapshot.key
if let friendIndex = self.myFriendsDataSource.firstIndex(where: { $0.uid == key} ) {
let friend = self.myFriendsDataSource[friendIndex]
print("found user \(friend.batteryStatus), updating")
self.myFriendsDataSource[friendIndex] = FriendClass(withSnaphot: snapshot)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
It's also better practice to update just the tableView data that has changed rather than reloading the whole tableView. You can probably use the array index to generate an IndexPath for the appropriate row and then just reload that row. Without seeing your tableView methods I can't be precise, but it'll probably look something like this:
let indexPath = IndexPath(row: friendIndex, section: 0)
DispatchQueue.main.async {
self.tableView.reloadRows(at: [indexPath], with: .automatic)
}

UICollectionView reloads data without calling reloadData()

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)
}

sorting cells in table view through buttons

I have a table view with data from API.
Each cell has several properties. What I want, is to sorting cells by chosen button (these three rectangles).
I dont know exactly how should I do it. I think I need tree methods for that, each for clicked button. But how to deal with sorting and reloading data?
Thanks for answer.
override func viewDidLoad() {
super.viewDidLoad()
callAlamo(url: urlAPI)
}
func callAlamo(url: String){
Alamofire.request(url).responseJSON(completionHandler: {
response in
self.parseData(JSONData: response.data!)
})
}
func parseData(JSONData: Data){
do{
var readableJSON = try JSONSerialization.jsonObject(with: JSONData, options: .mutableContainers) as! JSONStandard
// print(readableJSON)
if let rows = readableJSON["rows"] as? [JSONStandard]{
print(rows)
for i in 0..<rows.count{
let row = rows[i]
if let name = row["name"] as? String{
if name.isEmpty == false{
if let status = row["status"] as? String{
if let counter = row["counter"] as? String{
items.append(Station.init(place: name, counter: counter, status: status))
DispatchQueue.main.async {
self.tableView.reloadData()
}
}else{
let counter = "no data"
items.append(Station.init(place: name, stationsCount: counter, status: status))
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
}
}catch{
print(error)
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
cell.place.text = items[indexPath.row].place
cell.stationsCount.text = items[indexPath.row].stationsCount
let num = items[indexPath.row].status
let value = picDictionary[num!]
print(num!, value!)
cell.statusSign.image = UIImage(named: value!)
return cell
}
Sometimes I get null value from API, and then, I assing value to string "no data".
if let counter = row["counter"] as? String{
/////code
else{
let counter = "no data"
//////
}
And I dont want to let these values taking part in sorting process, because they are not numbers. How to do that?
Make three actions for your three buttons and sort the array with it and reload the table after that.
#IBAction func sortWithName(_ sender: UIButton) {
items.sort { $0.place < $1.place }
self.tableView.reloadData()
}
#IBAction func sortWithStatus(_ sender: UIButton) {
items.sort { $0.status < $1.status }
self.tableView.reloadData()
}
#IBAction func sortWithStatusCount(_ sender: UIButton) {
//stationsCount is looks like number so sort it using compare with numeric option
items.sort { $0.stationsCount.compare($1.stationsCount, options: .numeric) == .orderedAscending }
self.tableView.reloadData()
}
Edit: Declare one more array named allItems same way you have declared your items array. One more thing I haven't noticed first that you are reloading tableView inside for loop so that it will reload n(o) times instead of that you need to reload it once after the for loop also before reloading the tableView and set allItems with your items also you are making too many if condition where you can combine it in once.
for row in rows {
if let name = row["name"] as? String,
let status = row["status"] as? String,
!name.isEmpty {
if let counter = row["counter"] as? String {
items.append(Station.init(place: name, counter: counter, status: status))
}else{
let counter = "no data"
items.append(Station.init(place: name, stationsCount: counter, status: status))
}
}
}
//Set allItems with items
self.allItems = items
DispatchQueue.main.async {
self.tableView.reloadData()
}
Now change all your button actions like this.
ke three actions for your three buttons and sort the array with it and reload the table after that.
#IBAction func sortWithName(_ sender: UIButton) {
items = allItems.sorted { $0.place < $1.place }
self.tableView.reloadData()
}
#IBAction func sortWithStatus(_ sender: UIButton) {
items = allItems.sorted { $0.status < $1.status }
self.tableView.reloadData()
}
#IBAction func sortWithStatusCount(_ sender: UIButton) {
//Filter your array first
items = allItems.filter { $0.stationsCount != "no data" }
//stationsCount is looks like number so sort it using compare with numeric option
items.sort { $0.stationsCount.compare($1.stationsCount, options: .numeric) == .orderedAscending }
self.tableView.reloadData()
}

Retrieve from firebase database calculating average

I'm creating an iOS application that uses placenames and rating for each place. I have already made the thing work. I mean that, I save data to my database and I also can read them. The only problem, is when I read them I want them to load on my tableviewcell, by calculating average for each place. See the screenshots and if you don't understand something, ask me to edit the answer.
TableView
Firebase
My Code that loads data to tableview
import UIKit
import FirebaseDatabase
class PlacesTableViewController: UITableViewController {
//MARK: Properties
#IBOutlet weak var placesTableView: UITableView!
var dbRef:FIRDatabaseReference?
var places = [Places]()
private var loadedLabels = [String: String]()
private var loadedRatings = [String: Int]()
override func viewDidLoad()
{
super.viewDidLoad()
dbRef = FIRDatabase.database().reference()
// Loads data to cell.
loadData()
}
override func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
//return the number of rows
return places.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
// Table view cells are reused and should be dequeued using a cell identifier.
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? PlacesTableViewCell else {
fatalError("The dequeued cell is not an instance of PlacesTableView Cell.")
}
let place = places[indexPath.row]
cell.placeLabel.text = place.name
cell.ratingControl.rating = place.rating
return cell
}
private func loadData()
{
dbRef!.observe(.childAdded, with: {
(placeSnapshot) in
//print("Adding place \(placeSnapshot.key)...")
let labels = placeSnapshot.childSnapshot(forPath: "placeLabel")
for (key, label) in labels.value as! [String: String] {
self.updatePlace(key, label: label)
}
let ratings = placeSnapshot.childSnapshot(forPath: "rating")
for (key, rating) in ratings.value as! [String: Int] {
self.updatePlace(key, rating: rating)
}
})
}
private func updatePlace(_ key: String, label: String? = nil, rating: Int? = nil)
{
if let label = label {
loadedLabels[key] = label
}
if let rating = rating {
loadedRatings[key] = rating
}
guard let label = loadedLabels[key], let rating = loadedRatings[key] else {
return
}
if let place = Places(name: label, rating: rating) {
places.append(place)
placesTableView.reloadData()
}
}
}
Places swift
import UIKit
class Places {
//MARK: Properties
var name: String
var rating: Int
//MARK:Types
struct PropertyKey {
static let name = "name"
static let rating = "rating"
}
//MARK: Initialization
init?(name: String, rating: Int) {
// Initialize stored properties.
self.name = name
self.rating = rating
// Initialization should fail if there is no name or if the rating is negative.
// The name must not be empty
guard !name.isEmpty else {
return nil
}
// The rating must be between 0 and 5 inclusively
guard (rating >= 0) && (rating <= 5) else {
return nil
}
}
}
As i understood you want to have a rounded double for your application and not a double. Just change the code inside your loadData() function and it would work for you. Also you will call updatePlace() as you did. Please approve Jay's answer, he wrote the code.
private func loadData()
{
dbRef!.observe(.childAdded, with: {
(placeSnapshot) in
let parentRef = self.dbRef?.child(placeSnapshot.key)
let ratingRef = parentRef?.child("rating")
ratingRef?.observe(.value, with: { snapshot in
let count = snapshot.childrenCount
var total: Double = 0.0
for child in snapshot.children {
let snap = child as! FIRDataSnapshot
let val = snap.value as! Double
total += val
}
let average = total/Double(count)
print("Average for \(placeSnapshot.key) = \(Int(round(average)))")
self.updatePlace("" , label: placeSnapshot.key, rating: Int(round(average)))
})
})
}
This is a pretty verbose answer but it will iterate over the child nodes of the rating node and calculate the average
let parentRef = self.dbRef.child("Paradosiako - Panorama")
let ratingRef = parentRef.child("rating") // rating node
ratingRef.observe(.value, with: { snapshot in
let count = snapshot.childrenCount
var total: Double = 0.0
for child in snapshot.children {
let snap = child as! FIRDataSnapshot
let val = snap.value as! Double
total += val
}
let average = total/Double(count)
print(average)
})
EDIT
To iterate over all the places and get the averages, here's the code
let placesRef = self.dbRef.child("akapnapp")
placesRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let placeSnap = child as! FIRDataSnapshot
let ratingsSnap = placeSnap.childSnapshot(forPath: "rating")
let count = ratingsSnap.childrenCount
var total: Double = 0.0
for child in ratingsSnap.children {
print(child)
let snap = child as! FIRDataSnapshot
let val = snap.value as! Double
total += val
}
let average = total/Double(count)
print(average)
}
})
and the output showing the code works correctly. My child nodes were named a,b,c instead of the childByAutoId in the question but it works either way.
Snap (a) 3
Snap (b) 3
3.0
Snap (a) 2
Snap (b) 4
Snap (c) 5
3.67

Swift Compiler Warning : Result of call to 'save(defaults:)' is unused

So my table view is not loading anything and I think it's because of this warning that I get. It saids the save function is not being used so how can it load something that is not saved. What I am saving is the indexPath and Section of the row that the user selected via a button action in the row.
Warning:
Result of call to 'save(defaults:)' is unused
Code:
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 = DataHandling(dataId: dataId, indexPath: indexPath)
// Warning is here
ordering.save(defaults: indexPath.defaultsKey)
}
}
}
}
NSCoder Class for DataHandling / ordering.save
DataHandling.swift
class DataHandling: 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 box: String) -> Bool {
let defaults = UserDefaults.standard
let savedData = NSKeyedArchiver.archivedData(withRootObject: self)
defaults.set(savedData, forKey: box)
return defaults.synchronize()
}
convenience init?(defaults box: String) {
let defaults = UserDefaults.standard
if let data = defaults.object(forKey: box) as? Data,
let obj = NSKeyedUnarchiver.unarchiveObject(with: data) as? DataHandling,
let dataId = obj.dataId,
let indexPath = obj.indexPath {
self.init(dataId: dataId, indexPath: indexPath)
} else {
return nil
}
}
class func allSavedOrdering(_ maxRows: Int) -> [Int: [DataHandling]] {
var result: [Int: [DataHandling]] = [:]
for section in 0...1 {
var rows: [DataHandling] = []
for row in 0..<maxRows {
let indexPath = IndexPath(row: row, section: section)
if let ordering = DataHandling(defaults: indexPath.defaultsKey) {
rows.append(ordering)
}
rows.sort(by: { $0.indexPath! < $1.indexPath! })
}
result[section] = rows
}
return result
}
}
Other code I'm using:
// Number of Rows in Section
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items?[section].count ?? 0
}
// Number of Sections
func numberOfSections(in tableView: UITableView) -> Int {
return self.items?.count ?? 0
}
Saving it with:
saveSorting() { "\($0)" }
Loading it in ViewDidLoad:
func fetchData() {
// Load Data from Server to testArray
retrieveData()
// request from remote or local
data = [testArray]
// 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
myTableView.reloadData()
}
Loading Code:
// Loading
func applySorting(_ dataIdBlock: (Any) -> String) {
// get all saved ordering
guard let data = self.data else { return }
let ordering = DataHandling.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
}
The DataHandling instance's save(defaults:) function technically returns a value, even if you don't use it. To silence this warning, assign it to _ to signify that you don't intend to use the result value, e.g.:
_ = ordering.save(defaults: indexPath.defaultsKey)
or
let _ = ordering.save(defaults: indexPath.defaultsKey)
Just to be clear, this is almost definitely not why your tableview is not loading data. It should be pretty insignificant. The indexPath.defaultsKey is being saved (assuming the API works).

Resources