Hey everybody,
first of all, I'm new in Swift (but not new in software development).
I'm currently trying to create a private app which is used at work for another person.
It should be possible to create/add/edit or delete customers which are displayed in UITableView and Managed in UITableViewController.
I created a small caching system:
1. Fetch data from API (an URL which returns JSON)
2. Store data (in my case, an array of customers: [Customer]) on device (as JSON or is there any better way to store an array with custom objects?)
3. If user restarts app or refreshes table within X minutes, load data from cache, otherwise fetch again from API.
Fetching first time from the API works fine and the data is getting inserted into the table. But getting the data from cache, does not work even if it's exactly the same array [Customer].
I found out, that
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
is not even get called, and I have absolutely no idea why it works fetching from API but not from cache.
CustomerListController:
//
// CustomerController.swift
//
// Copyright © 2020 XXXXX. All rights reserved.
//
import UIKit
class CustomerListController: BaseTableController {
/// The table view instance
@IBOutlet var customerTableView: UITableView!
/// List of all customers, fetched from the API
var customersList: [Customer] = []
/// List of all customers, filtered by search controller
var filteredCustomersList: [Customer] = []
/// Array of customers, sorted in sections
var sectionCustomers: [Dictionary<String, [Customer]>.Element] = []
/// Array of filtered customers, sorted in sections
var filteredSectionCustomers: [Dictionary<String, [Customer]>.Element] = []
/// The search controller
var searchController: UISearchController!
/**
* viewDidLoad()
*/
override func viewDidLoad() {
super.viewDidLoad()
// self.customerTableView.tintColor = Constants.COLOR_ON_PRIMARY_BLUE
// self.navigationController?.navigationBar.barTintColor = .red
UITabBar.appearance().tintColor = Constants.COLOR_ON_PRIMARY_BLUE
UITabBar.appearance().barTintColor = Constants.COLOR_PRIMARY_BLUE
// Do any additional setup after loading the view.
self.customerTableView.delegate = self
self.customerTableView.dataSource = self
self.title = "Customers"
self.configureRefreshControl()
self.configureSearchController()
// @NOTE: SectionBar foreground color
self.customerTableView.tintColor = Constants.COLOR_PRIMARY_BLUE
// @NOTE: Loads the data into the table
self.loadData()
}
/**
* Loads the data from cache or refreshes it from the JSON API.
*/
func loadData() {
let current = Date()
let lastCustomerFetch = UserDefaults.standard.object(forKey: Constants.UD_CUSTOMER_LAST_FETCH) as? Date
print("Last fetch: \(lastCustomerFetch!)")
print("--------------------------------------------------------------------")
if lastCustomerFetch != nil {
if lastCustomerFetch!.distance(to: current) >= 900 {
self.fetchJSONDataAndUpdateTable(cachedAt: current, onFetchComplete: { () -> Void in
self.reloadTableData()
})
} else {
do {
let storedObjItem = UserDefaults.standard.object(forKey: Constants.UD_CUSTOMER_CACHE)
self.customersList = try JSONDecoder().decode([Customer].self, from: storedObjItem as! Data)
} catch {
Alert(title: "Error", message: error.localizedDescription).show()
}
print("Updated customers from cache (\(self.customersList.count) entries)")
print(self.customersList)
// @NOTE: Reload table view
self.reloadTableData()
}
} else {
self.fetchJSONDataAndUpdateTable(cachedAt: current, onFetchComplete: { () -> Void in
self.reloadTableData()
})
}
}
/**
* Fetches the json data from an url.
* - Parameter cachedAt: The caching time.
* - Parameter onFetchComplete: What happens, if the fetch has been completed.
*/
func fetchJSONDataAndUpdateTable(cachedAt: Date, onFetchComplete: @escaping () -> Void) {
// @NOTE: Direct JSON URL
var apiParameters = [
"method": "customers",
"limit": 25
]
guard let url = APIUtilities.buildAPIURL(parameters: &apiParameters) else {
Alert(title: "Error", message: "Failed to build api url.").show()
return
}
// @NOTE: Fetches the JSON from tha API-URL and handles it's result.
JSONUtilities.getJSONFromURL(url: url, handleURLData: { (data) in
do {
let decodedResponseJSON = try JSONDecoder().decode(GetCustomerJSONStruct.self, from: data)
// @NOTE: Store basic data in memory
self.customersList = decodedResponseJSON.data ?? []
// @NOTE: Sort into alphabetical sections
self.sectionCustomers = self.orderCustomersIntoSections()
// @NOTE: Store basic data in cache
if let encodedCustomers = try? JSONEncoder().encode(self.customersList) {
UserDefaults.standard.set(encodedCustomers, forKey: Constants.UD_CUSTOMER_CACHE)
}
UserDefaults.standard.set(cachedAt, forKey: Constants.UD_CUSTOMER_LAST_FETCH)
print("Updated customers from API (\(self.customersList.count) entries)")
print(self.customersList)
onFetchComplete()
} catch {
DispatchQueue.main.async {
Alert(title: "Error", message: "Failed to decode JSON.").show()
}
}
})
}
/*
* Reloads the table view containing the customer data.
*/
func reloadTableData() {
print("Refreshing data...")
DispatchQueue.main.async {
self.customerTableView.reloadData()
print("Refresh complete!")
}
}
/**
* Order the customers into alphabet-based bsections.
*/
func orderCustomersIntoSections() -> [Dictionary<String, [Customer]>.Element] {
var orderedSections: [String:[Customer]] = [:]
// @NOTE: Order customers into sections relative to the first character of their display name
for customer in self.customersList {
var firstDisplayCharacter = String(customer.getDisplayName().string.prefix(1)).uppercased()
let range = firstDisplayCharacter.rangeOfCharacter(from: CharacterSet.letters)
if range == nil {
firstDisplayCharacter = "#"
}
// @NOTE: Create new section if it doesn't exist yet
if orderedSections[firstDisplayCharacter] == nil {
orderedSections[firstDisplayCharacter] = []
}
orderedSections[firstDisplayCharacter]?.append(customer)
}
return orderedSections.sorted { (first, second) -> Bool in
return first.key < second.key
}
}
/**
* Adds the refresh control to the table view.
*/
func configureRefreshControl () {
self.customerTableView.refreshControl = UIRefreshControl()
self.customerTableView.refreshControl?.tintColor = Constants.COLOR_ON_PRIMARY_BLUE
self.customerTableView.refreshControl?.addTarget(self, action: #selector(handleRefreshControl), for: .valueChanged)
self.customerTableView.refreshControl?.attributedTitle = NSAttributedString(
string: "Pull down to refresh",
attributes: [.foregroundColor: Constants.COLOR_ON_PRIMARY_BLUE]
)
}
/**
* Adds the search field on top of the table.
*/
func configureSearchController() {
self.searchController = super.configureSearchController(
placeholder: "Search...",
scopeButtonTitles: [Constants.CUSTOMER_SEARCH_SCOPE_NAME, Constants.CUSTOMER_SEARCH_SCOPE_ADDRESS],
tintColor: Constants.COLOR_ON_PRIMARY_BLUE,
backgroundColor: Constants.COLOR_PRIMARY_BLUE
)
self.searchController.searchResultsUpdater = self
self.searchController.searchBar.delegate = self
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
}
/**
* Handles the table view refresh control.
*/
@objc func handleRefreshControl() {
// @TODO: Refresh
// Dismiss the refresh control.
DispatchQueue.main.async {
self.customerTableView.refreshControl?.endRefreshing()
}
}
/**
* Filters the customers by the given scope.
*/
func filterCustomersForSearch(searchText: String, scope: String) {
let lowercasedSearchText = searchText.lowercased()
self.filteredSectionCustomers = []
if self.isSearchBarEmpty() {
self.filteredSectionCustomers = self.sectionCustomers
} else {
switch(scope) {
case Constants.CUSTOMER_SEARCH_SCOPE_NAME:
for (sectionTitle, entry) in self.sectionCustomers {
var tempCustomers: [Customer] = []
for customer in entry {
if customer.getDisplayName().string.lowercased().contains(lowercasedSearchText) {
tempCustomers.append(customer)
}
}
if tempCustomers.count > 0 {
self.filteredSectionCustomers.append((sectionTitle, tempCustomers))
}
}
break;
case Constants.CUSTOMER_SEARCH_SCOPE_ADDRESS:
for (sectionTitle, entry) in self.sectionCustomers {
var tempCustomers: [Customer] = []
for customer in entry {
if customer.getDisplayAddress().lowercased().contains(lowercasedSearchText) {
tempCustomers.append(customer)
}
}
if tempCustomers.count > 0 {
self.filteredSectionCustomers.append((sectionTitle, tempCustomers))
}
}
break;
default:
break;
}
}
self.reloadTableData()
}
/**
* - Returns: TRUE, if the search bar is empty, otherwise FALSE.
*/
func isSearchBarEmpty() -> Bool {
return self.searchController.searchBar.text?.isEmpty ?? true
}
/**
* - Returns: TRUE, if the user is searching, otherwise FALSE.
*/
func isUserSearching() -> Bool {
let searchBarScopeIsFiltering = self.searchController.searchBar.selectedScopeButtonIndex != 0
return self.searchController.isActive && (!self.isSearchBarEmpty() || searchBarScopeIsFiltering)
}
}
extension CustomerListController {
/**
* This is called for every table cell.
*/
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "CustomerCell") as? CustomerCell else { return UITableViewCell() }
let currentCustomer = self.isUserSearching()
? self.filteredSectionCustomers[indexPath.section].value[indexPath.row]
: self.sectionCustomers[indexPath.section].value[indexPath.row]
cell.customerDisplayName.attributedText = currentCustomer.getDisplayName()
cell.customerDisplayAddress.text = currentCustomer.getDisplayAddress()
return cell
}
/**
* Returns the amount of sections.
*/
override func numberOfSections(in tableView: UITableView) -> Int {
return self.isUserSearching() ? self.filteredSectionCustomers.count : self.sectionCustomers.count
}
/**
* Returns the amount of rows in a section.
*/
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.isUserSearching()
? self.filteredSectionCustomers[section].value.count
: self.sectionCustomers[section].value.count
}
/**
* Set's the title for sections.
*/
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return (ListUtilities.getKeysFromTuples(
tuple: (self.isUserSearching() ? self.filteredSectionCustomers : self.sectionCustomers)
) as? [String])?[section]
}
/**
* The section index title on the right side of the table view.
*/
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return ListUtilities.getKeysFromTuples(
tuple: self.isUserSearching() ? self.filteredSectionCustomers : self.sectionCustomers
) as? [String]
}
/**
* Called, whenever a user deselectes a table cell.
*/
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "showCustomerDetail", sender: self)
}
/**
* Prepares the segue to open.
*/
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? CustomerDetailController {
let indexPath = self.customerTableView.indexPathForSelectedRow
guard let selectedSection = indexPath?.section else { return }
guard let selectedRow = indexPath?.row else { return }
let customerList = self.isUserSearching()
? self.filteredSectionCustomers
: self.sectionCustomers
destination.customer = customerList[selectedSection].value[selectedRow]
}
}
}
extension CustomerListController: UISearchBarDelegate, UISearchResultsUpdating {
/**
* Called when the suer changes the search scope.
*/
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
self.filterCustomersForSearch(searchText: searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
}
/**
* Called when the search results need tp be updated (e.g. when the user types a character in the search bar)
*/
func updateSearchResults(for searchController: UISearchController) {
let searchBar = self.searchController!.searchBar
let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
self.filterCustomersForSearch(searchText: self.searchController!.searchBar.text!, scope: scope)
}
}
Class "BaseTableController" extends from UITableViewController and does not contain important things.
I hope, you can help me and I reall appreciate your support.
Thank you!