Using swipe action to delete files in Swift 4 - ios

I have the following function in my app to allow for a Swipe Action to clear the related images (not to remove the row, just some locally stored files). When try it, its using the default "XXX_1.jpg" value, rather than the bonusName that it should be using.
Here is my swipeAction code:
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let clearAction = UIContextualAction(style: .normal, title: "Clear Data") { (contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in
print("Clear Action Tapped")
// Delete the created images
let fileNameToDeletePri = "\(self.bonus?.bonusCode ?? "XXX")_1.jpg"
let fileNameToDeleteOpt = "\(self.bonus?.bonusCode ?? "XXX")_2.jpg"
var filePathPri = ""
var filePathOpt = ""
// Find documents directory on device
let dirs : [String] = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.allDomainsMask, true)
if dirs.count > 0 {
let dir = dirs[0] //documents directory
filePathPri = dir.appendingFormat("/" + fileNameToDeletePri)
filePathOpt = dir.appendingFormat("/" + fileNameToDeleteOpt)
print("Local path = \(filePathPri)")
print("Local path = \(filePathOpt)")
} else {
print("Could not find local directory to store file")
return
}
do {
let fileManager = FileManager.default
// Check if primary file exists
if fileManager.fileExists(atPath: filePathPri) {
// Delete file
try fileManager.removeItem(atPath: filePathPri)
} else {
print("Primary image does not exist")
}
// Check if optional file exists
if fileManager.fileExists(atPath: filePathOpt) {
// Delete file
try fileManager.removeItem(atPath: filePathOpt)
} else {
print("Optional image does not exist")
}
}
catch let error as NSError {
print("An error took place: \(error)")
}
completionHandler(true)
}
clearAction.backgroundColor = .blue
let swipeConfig = UISwipeActionsConfiguration(actions: [clearAction])
return swipeConfig
}
Oddly, I had initially added the above code in my unwindAction on my UITableDetailView and it worked, but decided I didn't want to delete the images anytime someone hit cancel, so I moved it to be a swipe action within my UITableView. I suspect it isn't figuring out which row I'm on and therefore defaulting to the XXX option.
EDIT: Here is the full ViewController, so you can see the above in context:
import UIKit
import os.log
import Foundation
class BonusListViewController: UITableViewController {
var bonuses = [JsonFile.JsonBonuses]()
var bonus: JsonFile.JsonBonuses?
var filteredBonuses = [JsonFile.JsonBonuses]()
var detailViewController: BonusDetailViewController? = nil
var riderNumToH:String = UserDefaults.standard.string(forKey: Constants.RiderData().riderNumToH)!
var pillionNumToH:String = UserDefaults.standard.string(forKey: Constants.RiderData().pillionNumToH)!
var emailDestinationToH:String = UserDefaults.standard.string(forKey: Constants.RallyData().emailDestinationToH)!
struct Constants {
struct RiderData {
let riderNumToH = "riderNumToH"
let pillionNumToH = "pillionNumToH"
}
struct RallyData {
let emailDestinationToH = "emailDestinationToH"
}
}
let defaults = UserDefaults.standard
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
// MARK: Search Support
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Enter two letter state to filter"
navigationItem.searchController = searchController
definesPresentationContext = true
// MARK: Settings Data Struct
struct Constants {
struct RiderData {
let riderNumToH = "riderNumToH"
let pillionNumToH = "pillionNumToH"
}
struct RallyData {
let emailDestinationToH = "emailDestinationToH"
}
}
//MARK: Load the bonuses
print("About to call loadBonuses")
loadBonuses { [weak self] bonuses in
self?.bonuses = bonuses ?? []
DispatchQueue.main.async {
self?.tableView.reloadData()
}
print("loadBonuses called")
}
// MARK: Set Rider Defaults to Initial Values.
let defaults = UserDefaults.standard
print("Setting initial defaults")
if riderNumToH == "" {
print("riderNumToH is blank")
defaults.set("000", forKey: Constants.RiderData().riderNumToH)
} else if riderNumToH == "000" {
print("riderNumToH is 000")
} else {
print("riderNumToH is custom")
}
if pillionNumToH == "" {
print("pillionNumToH is blank")
defaults.set("000", forKey: Constants.RiderData().pillionNumToH)
} else if pillionNumToH == "000" {
print("pillionNumToH is 000")
} else {
print("pillionNumToH is custom")
}
if emailDestinationToH == "" {
print("emailDestinationToH is blank")
defaults.set("photos#tourofhonor.com", forKey: Constants.RallyData().emailDestinationToH)
} else if emailDestinationToH == "photos#tourofhonor.com" {
print("emailDestinationToH is set to default")
} else {
print("emailDestinationToH has been customized")
}
}
// MARK: - Table View Configuration
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
print("Showing \(filteredBonuses.count) Filtered Results")
return filteredBonuses.count
}
print("Found \(bonuses.count) rows in section.")
return bonuses.count
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let clearAction = UIContextualAction(style: .normal, title: "Clear Data") { (contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in
print("Clear Action Tapped")
// Delete the created images
print("Selected Bonus is \(self.bonus?.bonusCode ?? "XXX")")
let fileNameToDeletePri = "\(self.bonus?.bonusCode ?? "XXX")_1.jpg"
let fileNameToDeleteOpt = "\(self.bonus?.bonusCode ?? "XXX")_2.jpg"
var filePathPri = ""
var filePathOpt = ""
// Find documents directory on device
let dirs : [String] = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.allDomainsMask, true)
if dirs.count > 0 {
let dir = dirs[0] //documents directory
filePathPri = dir.appendingFormat("/" + fileNameToDeletePri)
filePathOpt = dir.appendingFormat("/" + fileNameToDeleteOpt)
print("Local path = \(filePathPri)")
print("Local path = \(filePathOpt)")
} else {
print("Could not find local directory to store file")
return
}
do {
let fileManager = FileManager.default
// Check if primary file exists
if fileManager.fileExists(atPath: filePathPri) {
// Delete file
try fileManager.removeItem(atPath: filePathPri)
} else {
print("Primary image does not exist")
}
// Check if optional file exists
if fileManager.fileExists(atPath: filePathOpt) {
// Delete file
try fileManager.removeItem(atPath: filePathOpt)
} else {
print("Optional image does not exist")
}
}
catch let error as NSError {
print("An error took place: \(error)")
}
completionHandler(true)
}
clearAction.backgroundColor = .blue
let swipeConfig = UISwipeActionsConfiguration(actions: [clearAction])
return swipeConfig
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "BonusListViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BonusListViewCell else {
fatalError("The dequeued cell is not an instance of BonusListViewCell.")
}
// let bonus = bonuses[indexPath.row]
let bonus: JsonFile.JsonBonuses
if isFiltering() {
bonus = filteredBonuses[indexPath.row]
} else {
bonus = bonuses[indexPath.row]
}
// Set Primary Image
let documentsUrl = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
let imgUrl = documentsUrl.appendingPathComponent(bonus.bonusCode + "_1.jpg")
if(FileManager.default.fileExists(atPath:imgUrl.path))
{
do
{
let data = try Data.init(contentsOf:imgUrl)
cell.primaryImage.image = UIImage.init(data:data)
}
catch {
print(error)
}
}
else
{
cell.primaryImage.image = #imageLiteral(resourceName: "DefaultImage")
}
//let urlString = "http://tourofhonor.com/appimages/"+(bonus.imageName)
//let url = URL(string: urlString)
//cell.primaryImage.downloadedFrom(url: url!)
cell.nameLabel.text = bonus.name.capitalized
cell.bonusCodeLabel.text = bonus.bonusCode.localizedUppercase
cell.categoryLabel.text = bonus.category
cell.valueLabel.text = "\(bonus.value)"
cell.cityLabel.text = "\(bonus.city.capitalized),"
cell.stateLabel.text = bonus.state.localizedUppercase
return cell
}
// MARK: Functions
// MARK: - Fetch JSON from ToH webserver
func downloadJSON(completed: #escaping ([JsonFile.JsonBonuses]?) -> ()) {
let url = URL(string: "http://tourofhonor.com/BonusData.json")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error == nil, let data = data {
do {
let posts = try JSONDecoder().decode(JsonFile.self, from: data)
completed(posts.bonuses)
self.defaults.set(posts.meta.version, forKey: "jsonVersion")
print("URLSession did not fail")
print("JSON Version Set to \(posts.meta.version)")
} catch {
print("Can't decode JSON: \(error)")
}
} else {
print("downloadJSON completed")
completed(nil)
}
}.resume()
}
func saveBonuses(_ bonuses: [JsonFile.JsonBonuses], to url: URL) {
try? FileManager.default.removeItem(at: url)
do {
let data = try JSONEncoder().encode(bonuses)
try data.write(to: url)
print("saveBonuses successful")
} catch {
print("Error saving bonuses to file:", error)
}
}
func loadBonusesFromFile(_ url: URL) -> [JsonFile.JsonBonuses]? {
do {
let data = try Data(contentsOf: url)
let bonuses = try JSONDecoder().decode([JsonFile.JsonBonuses].self, from: data)
print("loadBonusesFromFile successful")
return bonuses
} catch {
print("Error loading bonuses from file:", error)
return nil
}
}
func loadBonuses(completion: #escaping ([JsonFile.JsonBonuses]?) -> Void) {
let localBonusesURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("BonusData.json")
downloadJSON { bonuses in
if let bonuses = bonuses {
completion(bonuses)
self.saveBonuses(bonuses, to: localBonusesURL)
} else {
print("versions did not match")
completion(self.loadBonusesFromFile(localBonusesURL))
}
}
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredBonuses = bonuses.filter({( bonus: JsonFile.JsonBonuses) -> Bool in
return bonus.state.localizedCaseInsensitiveContains(searchText)
})
tableView.reloadData()
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? BonusDetailViewController {
destination.bonus = bonuses[(tableView.indexPathForSelectedRow?.row)!]
}
}
}
extension BonusListViewController: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}

You are creating a new variable with the name bonus so the class variable (self.bonus) is left out to be empty.
You can remove self.bonus in tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? and replace it with the same implementation in forRowAt
let bonus: JsonFile.JsonBonuses
if isFiltering() {
bonus = filteredBonuses[indexPath.row]
} else {
bonus = bonuses[indexPath.row]
}
In this case you will be sure that the bonus is correct and ignore the class variable.

Related

How to fix freezing Active Indicator when pulling to refresh in the Table View

Every time when I use pull to refresh, the Active Indicator and the application itself freeze for a moment. I found which method is causing this problem - getTracks(), if I remove it then everything works well (except that the table is not updated), but I can't figure out what's the matter, I've tried many options, but no result. Can anyone help me?
My code:
class LibraryViewController: UIViewController {
private let fetchManager = FetchManager()
private var tracks: [TrackModelProtocol] = []
...
private let refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshTableView), for: .valueChanged)
return refreshControl
}()
...
override func viewDidLoad() {
super.viewDidLoad()
...
getTracks()
setupTableView()
tableView.refreshControl = refreshControl
}
...
private func getTracks() {
guard let tracks = fetchManager.setupTrackModels() else { return }
self.tracks = tracks
}
//pull to refresh
#objc private func refreshTableView(sender: UIRefreshControl) {
getTracks()
tableView.reloadData()
sender.endRefreshing()
}
...
}
...
class FetchManager {
var content: [URL] = []
private var currentDirectory : URL?
private var playerQueue: [AVPlayerItem] = []
private let audioPlayer = AudioPlayer()
private func fetchDataFromDeviceMemory() -> [URL]? {
var content: [URL] = []
if currentDirectory == nil {
currentDirectory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
}
do {
let allURLs = try FileManager.default.contentsOfDirectory(at: currentDirectory!, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants])
content = allURLs.filter{ $0.pathExtension == "mp3" || $0.pathExtension == "aac" || $0.pathExtension == "wav" }
} catch {
content = []
}
return content
}
func setupTrackModels() -> [TrackModelProtocol]? {
guard let content = fetchDataFromDeviceMemory() else { return [] }
self.content = content
playerQueue = audioPlayer.createPlayerQueue(from: content)
var tracks: [TrackModelProtocol] = []
for index in 0..<playerQueue.count {
var artwork: UIImage?
let playerItem = playerQueue[index]
let metadataList = playerItem.asset.metadata
for item in metadataList {
guard let key = item.commonKey?.rawValue, let value = item.value else{
continue
}
switch key {
case "artwork" where value is Data:
artwork = UIImage(data: value as! Data)
default:
continue
}
}
let track = TrackFromDeviceMemory(
fileName: content[index].lastPathComponent,
duration: playerQueue[index].asset.duration.seconds,
artwork: artwork,
url: content[index].absoluteURL)
tracks.append(track)
}
return tracks
}
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if isSearchActive {
let deletedTrackURL = filteredTracks[indexPath.row].url
print(deletedTrackURL)
do {
try FileManager.default.removeItem(at: deletedTrackURL)
} catch {
print(error.localizedDescription)
}
getTracks()
searchController.isActive = false
// tableView.reloadData()
tableView.delegate?.tableView!(tableView, didSelectRowAt: IndexPath(row: 0, section: indexPath.section))
} else {
let deletedTrackURL = tracks[indexPath.row].url
print(deletedTrackURL)
do {
try FileManager.default.removeItem(at: deletedTrackURL)
} catch {
print(error.localizedDescription)
}
getTracks()
tableView.deleteRows(at: [indexPath], with: .automatic)
// tableView.reloadData()
Instead of returning an array with setupTrackModels, have you tried using a completion handler and perform the fetch on a global queue? Performing the fetch on the main queue itself might be the reason for freeze.
func setupTrackModels(completion: (_ models: [TrackModelProtocol])->Void) {
DispatchQueue.global().async {
guard let content = fetchDataFromDeviceMemory() else {
DispatchQueue.main.async {
completion([])
return
}
}
self.content = content
playerQueue = audioPlayer.createPlayerQueue(from: content)
var tracks: [TrackModelProtocol] = []
for index in 0..<playerQueue.count {
var artwork: UIImage?
let playerItem = playerQueue[index]
let metadataList = playerItem.asset.metadata
for item in metadataList {
guard let key = item.commonKey?.rawValue, let value = item.value else{
continue
}
switch key {
case "artwork" where value is Data:
artwork = UIImage(data: value as! Data)
default:
continue
}
}
let track = TrackFromDeviceMemory(
fileName: content[index].lastPathComponent,
duration: playerQueue[index].asset.duration.seconds,
artwork: artwork,
url: content[index].absoluteURL)
tracks.append(track)
}
DispatchQueue.main.async {
completion(tracks)
}
}
}
At the call site:
fetchManager.setupTrackModels { tracks in
refreshControl.endRefreshing()
self.tracks = tracks
tableView.reloadData()
}

Index related error in retrieving the data from Firestore database

I am not able to load the documents in chat application in Swift IOS using Firestore database, though able to successfully retrieve the data from the Firestore database, I have added the deinit method as well please assist further to resolve the error, I have added the complete view controller , please help me
Error
'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (47) must be equal to the number of rows contained in that section before the update (23), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
Code
let kBannerAdUnitID = "ca-app-pub-3940256099942544/2934735716"
#objc(FCViewController)
class FCViewController: UIViewController, UITableViewDataSource, UITableViewDelegate,
UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
// Instance variables
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var sendButton: UIButton!
var ref : CollectionReference!
var ref2: DocumentReference!
var messages: [DocumentSnapshot]! = []
var msglength: NSNumber = 10
fileprivate var _refHandle: CollectionReference!
var storageRef: StorageReference!
var remoteConfig: RemoteConfig!
private let db = Firestore.firestore()
private var reference: CollectionReference?
private let storage = Storage.storage().reference()
// private var messages = [Constants.MessageFields]()
//snapshot private var messages: [Constants.MessageFields] = []
private var messageListener: ListenerRegistration?
// var db:Firestore!
#IBOutlet weak var banner: GADBannerView!
#IBOutlet weak var clientTable: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.clientTable.register(UITableViewCell.self, forCellReuseIdentifier: "tableViewCell")
// clientTable.delegate = self
//clientTable.dataSource = self
//db = Firestore.firestore()
ref = db.collection("messages").document("hello").collection("newmessages").document("2").collection("hellos").document("K").collection("messages")
ref2 = db.collection("messages").document("hello").collection("newmessages").document("2").collection("hellos").document("K").collection("messages").document()
configureDatabase()
configureStorage()
configureRemoteConfig()
fetchConfig()
loadAd()
}
deinit {
if let refhandle = _refHandle {
let listener = ref.addSnapshotListener { querySnapshot, error in
}
listener.remove()
}
}
func configureDatabase() {
db.collection("messages").document("hello").collection("newmessages").document("2").collection("hellos").document("K").collection("messages").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
/* let name = documents.map { $0["name"]!}
let text = documents.map { $0["text"]!}
let photourl = documents.map { $0["photoUrl"]!}
print(name)
print(text)
print(photourl)*/
self.messages.append(contentsOf: documents)
// self.clientTable.insertRows(at: [IndexPath(row: self.messages.count-1, section: 0)], with: .automatic)
//self.clientTable.reloadData()
}
}
func configureStorage() {
storageRef = Storage.storage().reference()
}
func configureRemoteConfig() {
remoteConfig = RemoteConfig.remoteConfig()
let remoteConfigSettings = RemoteConfigSettings(developerModeEnabled: true)
remoteConfig.configSettings = remoteConfigSettings
}
func fetchConfig() {
var expirationDuration: Double = 3600
// If in developer mode cacheExpiration is set to 0 so each fetch will retrieve values from
// the server.
if self.remoteConfig.configSettings.isDeveloperModeEnabled {
expirationDuration = 0
}
remoteConfig.fetch(withExpirationDuration: expirationDuration) { [weak self] (status, error) in
if status == .success {
print("Config fetched!")
guard let strongSelf = self else { return }
strongSelf.remoteConfig.activateFetched()
let friendlyMsgLength = strongSelf.remoteConfig["friendly_msg_length"]
if friendlyMsgLength.source != .static {
strongSelf.msglength = friendlyMsgLength.numberValue!
print("Friendly msg length config: \(strongSelf.msglength)")
}
} else {
print("Config not fetched")
if let error = error {
print("Error \(error)")
}
}
}
}
#IBAction func didPressFreshConfig(_ sender: AnyObject) {
fetchConfig()
}
#IBAction func didSendMessage(_ sender: UIButton) {
_ = textFieldShouldReturn(textField)
}
#IBAction func didPressCrash(_ sender: AnyObject) {
print("Crash button pressed!")
Crashlytics.sharedInstance().crash()
}
func inviteFinished(withInvitations invitationIds: [String], error: Error?) {
if let error = error {
print("Failed: \(error.localizedDescription)")
} else {
print("Invitations sent")
}
}
func loadAd() {
self.banner.adUnitID = kBannerAdUnitID
self.banner.rootViewController = self
self.banner.load(GADRequest())
}
// UITableViewDataSource protocol methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue cell
let cell = self.clientTable .dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
// Unpack message from Firebase DataSnapshot
let messageSnapshot: DocumentSnapshot! = self.messages[indexPath.row]
guard let message = messageSnapshot as? [String:String] else { return cell }
let name = message[Constants.MessageFields.name] ?? ""
if let imageURL = message[Constants.MessageFields.imageURL] {
if imageURL.hasPrefix("gs://") {
Storage.storage().reference(forURL: imageURL).getData(maxSize: INT64_MAX) {(data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
DispatchQueue.main.async {
cell.imageView?.image = UIImage.init(data: data!)
cell.setNeedsLayout()
}
}
} else if let URL = URL(string: imageURL), let data = try? Data(contentsOf: URL) {
cell.imageView?.image = UIImage.init(data: data)
}
cell.textLabel?.text = "sent by: \(name)"
} else {
let text = message[Constants.MessageFields.text] ?? ""
cell.textLabel?.text = name + ": " + text
cell.imageView?.image = UIImage(named: "ic_account_circle")
if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
let data = try? Data(contentsOf: URL) {
cell.imageView?.image = UIImage(data: data)
}
}
return cell
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard let text = textField.text else { return true }
textField.text = ""
view.endEditing(true)
let data = [Constants.MessageFields.text: text]
sendMessage(withData: data)
return true
}
func sendMessage(withData data: [String: String]) {
var mdata = data
mdata[Constants.MessageFields.name] = Auth.auth().currentUser?.displayName
if let photoURL = Auth.auth().currentUser?.photoURL {
mdata[Constants.MessageFields.photoURL] = photoURL.absoluteString
}
// Push data to Firebase Database
self.ref.document().setData(mdata, merge: true) { (err) in
if let err = err {
print(err.localizedDescription)
}
print("Successfully set newest city data")
}
}
// MARK: - Image Picker
#IBAction func didTapAddPhoto(_ sender: AnyObject) {
let picker = UIImagePickerController()
picker.delegate = self
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) {
picker.sourceType = .camera
} else {
picker.sourceType = .photoLibrary
}
present(picker, animated: true, completion:nil)
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true, completion:nil)
guard let uid = Auth.auth().currentUser?.uid else { return }
// if it's a photo from the library, not an image from the camera
if #available(iOS 8.0, *), let referenceURL = info[.originalImage] as? URL {
let assets = PHAsset.fetchAssets(withALAssetURLs: [referenceURL], options: nil)
let asset = assets.firstObject
asset?.requestContentEditingInput(with: nil, completionHandler: { [weak self] (contentEditingInput, info) in
let imageFile = contentEditingInput?.fullSizeImageURL
let filePath = "\(uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000))/\((referenceURL as AnyObject).lastPathComponent!)"
guard let strongSelf = self else { return }
strongSelf.storageRef.child(filePath)
.putFile(from: imageFile!, metadata: nil) { (metadata, error) in
if let error = error {
let nsError = error as NSError
print("Error uploading: \(nsError.localizedDescription)")
return
}
strongSelf.sendMessage(withData: [Constants.MessageFields.imageURL: strongSelf.storageRef.child((metadata?.path)!).description])
}
})
} else {
guard let image = info[.originalImage] as? UIImage else { return }
let imageData = image.jpegData(compressionQuality:0.8)
let imagePath = "\(uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000)).jpg"
let metadata = StorageMetadata()
metadata.contentType = "image/jpeg"
self.storageRef.child(imagePath)
.putData(imageData!, metadata: metadata) { [weak self] (metadata, error) in
if let error = error {
print("Error uploading: \(error)")
return
}
guard let strongSelf = self else { return }
strongSelf.sendMessage(withData: [Constants.MessageFields.imageURL: strongSelf.storageRef.child((metadata?.path)!).description])
}
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion:nil)
}
#IBAction func signOut(_ sender: UIButton) {
let firebaseAuth = Auth.auth()
do {
try firebaseAuth.signOut()
dismiss(animated: true, completion: nil)
} catch let signOutError as NSError {
print ("Error signing out: \(signOutError.localizedDescription)")
}
}
func showAlert(withTitle title: String, message: String) {
DispatchQueue.main.async {
let alert = UIAlertController(title: title,
message: message, preferredStyle: .alert)
let dismissAction = UIAlertAction(title: "Dismiss", style: .destructive, handler: nil)
alert.addAction(dismissAction)
self.present(alert, animated: true, completion: nil)
}
}
}
Edit
perform this block of code on main thread
for doc in documents {
self.messages.append(doc)
self.clientTable.insertRows(at: [IndexPath(row: self.messages.count-1, section: 0)], with: .automatic)
}
This should work..

Filtered view is leading to incorrect detail view when tapped on, but correct detail view when swiped

I think this is my last question before I can finally 1.0 release this thing. Here's my last remaining "can't launch yet" issue: When I filter by state, the resulting rows are not opening the appropriate detail view.
For example, if I search for TX, I see the seven Texas bonuses. If I tap on the 3rd one, I end up in AK3 (which is the 3rd item in the list if it isn't being filtered). The weird part, is that my swipe action DOES know that I am on TX3. Only when tapping to go to the detail does it jump to the wrong bonus.
Here is the full UITableViewController page:
import UIKit
import os.log
import Foundation
class BonusListViewController: UITableViewController {
var bonuses = [JsonFile.JsonBonuses]()
var bonus: JsonFile.JsonBonuses?
var filteredBonuses = [JsonFile.JsonBonuses]()
var detailViewController: BonusDetailViewController? = nil
var riderNumToH:String = UserDefaults.standard.string(forKey: Constants.RiderData().riderNumToH) ?? "000"
var pillionNumToH:String = UserDefaults.standard.string(forKey: Constants.RiderData().pillionNumToH) ?? "000"
var emailDestinationToH:String = UserDefaults.standard.string(forKey: Constants.RallyData().emailDestinationToH) ?? "photos#tourofhonor.com"
struct Constants {
struct RiderData {
let riderNumToH = "riderNumToH"
let pillionNumToH = "pillionNumToH"
}
struct RallyData {
let emailDestinationToH = "emailDestinationToH"
}
}
let defaults = UserDefaults.standard
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
// MARK: Search Support
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Enter two letter state"
navigationItem.searchController = searchController
definesPresentationContext = true
// MARK: Settings Data Struct
struct Constants {
struct RiderData {
let riderNumToH = "riderNumToH"
let pillionNumToH = "pillionNumToH"
}
struct RallyData {
let emailDestinationToH = "emailDestinationToH"
}
}
//MARK: Load the bonuses
print("About to call loadBonuses")
loadBonuses { [weak self] bonuses in
self?.bonuses = bonuses ?? []
DispatchQueue.main.async {
self?.tableView.reloadData()
}
print("loadBonuses called")
}
// MARK: Set Rider Defaults to Initial Values.
let defaults = UserDefaults.standard
print("Setting initial defaults")
if riderNumToH == "" {
print("riderNumToH is blank")
defaults.set("000", forKey: Constants.RiderData().riderNumToH)
} else if riderNumToH == "000" {
print("riderNumToH is 000")
} else {
print("riderNumToH is custom")
}
if pillionNumToH == "" {
print("pillionNumToH is blank")
defaults.set("000", forKey: Constants.RiderData().pillionNumToH)
} else if pillionNumToH == "000" {
print("pillionNumToH is 000")
} else {
print("pillionNumToH is custom")
}
if emailDestinationToH == "" {
print("emailDestinationToH is blank")
defaults.set("photos#tourofhonor.com", forKey: Constants.RallyData().emailDestinationToH)
} else if emailDestinationToH == "photos#tourofhonor.com" {
print("emailDestinationToH is set to default")
} else {
print("emailDestinationToH has been customized")
}
}
// MARK: - Table View Configuration
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
print("Showing \(filteredBonuses.count) Filtered Results")
return filteredBonuses.count
}
print("Found \(bonuses.count) rows in section.")
return bonuses.count
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let clearAction = UIContextualAction(style: .normal, title: "Clear Data") { (contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in
print("Clear Action Tapped")
// Delete the created images
let bonus: JsonFile.JsonBonuses
if self.isFiltering() {
bonus = self.filteredBonuses[indexPath.row]
} else {
bonus = self.bonuses[indexPath.row]
}
print("Selected Bonus is \(bonus.bonusCode)")
let fileNameToDeletePri = "\(bonus.bonusCode)_1.jpg"
let fileNameToDeleteOpt = "\(bonus.bonusCode)_2.jpg"
var filePathPri = ""
var filePathOpt = ""
// Find documents directory on device
let dirs : [String] = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.allDomainsMask, true)
if dirs.count > 0 {
let dir = dirs[0] //documents directory
filePathPri = dir.appendingFormat("/" + fileNameToDeletePri)
filePathOpt = dir.appendingFormat("/" + fileNameToDeleteOpt)
print("Local path = \(filePathPri)")
print("Local path = \(filePathOpt)")
} else {
print("Could not find local directory to store file")
return
}
do {
let fileManager = FileManager.default
// Check if primary file exists
if fileManager.fileExists(atPath: filePathPri) {
// Delete file
try fileManager.removeItem(atPath: filePathPri)
} else {
print("Primary image does not exist")
}
// Check if optional file exists
if fileManager.fileExists(atPath: filePathOpt) {
// Delete file
try fileManager.removeItem(atPath: filePathOpt)
} else {
print("Optional image does not exist")
}
}
catch let error as NSError {
print("An error took place: \(error)")
}
tableView.reloadData()
completionHandler(true)
}
clearAction.backgroundColor = .blue
let swipeConfig = UISwipeActionsConfiguration(actions: [clearAction])
return swipeConfig
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "BonusListViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BonusListViewCell else {
fatalError("The dequeued cell is not an instance of BonusListViewCell.")
}
let bonus: JsonFile.JsonBonuses
if self.isFiltering() {
bonus = self.filteredBonuses[indexPath.row]
} else {
bonus = self.bonuses[indexPath.row]
}
// Set Primary Image
let documentsUrl = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
let imgUrl = documentsUrl.appendingPathComponent(bonus.bonusCode + "_1.jpg")
if(FileManager.default.fileExists(atPath:imgUrl.path))
{
do
{
let data = try Data.init(contentsOf:imgUrl)
cell.primaryImage.image = UIImage.init(data:data)
}
catch {
print(error)
}
}
else
{
cell.primaryImage.image = #imageLiteral(resourceName: "DefaultImage")
}
cell.nameLabel.text = bonus.name
cell.bonusCodeLabel.text = bonus.bonusCode.localizedUppercase
cell.categoryLabel.text = bonus.category
cell.valueLabel.text = "\(bonus.value)"
cell.cityLabel.text = "\(bonus.city.capitalized),"
cell.stateLabel.text = bonus.state.localizedUppercase
return cell
}
// MARK: Functions
// MARK: - Fetch JSON from ToH webserver
func downloadJSON(completed: #escaping ([JsonFile.JsonBonuses]?) -> ()) {
let url = URL(string: "http://tourofhonor.com/BonusData.json")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error == nil, let data = data {
do {
let posts = try JSONDecoder().decode(JsonFile.self, from: data)
completed(posts.bonuses)
self.defaults.set(posts.meta.version, forKey: "jsonVersion")
print("URLSession did not fail")
print("JSON Version Set to \(posts.meta.version)")
} catch {
print("Can't decode JSON: \(error)")
}
} else {
print("downloadJSON completed")
completed(nil)
}
}.resume()
}
func saveBonuses(_ bonuses: [JsonFile.JsonBonuses], to url: URL) {
try? FileManager.default.removeItem(at: url)
do {
let data = try JSONEncoder().encode(bonuses)
try data.write(to: url)
print("saveBonuses successful")
} catch {
print("Error saving bonuses to file:", error)
}
}
func loadBonusesFromFile(_ url: URL) -> [JsonFile.JsonBonuses]? {
do {
let data = try Data(contentsOf: url)
let bonuses = try JSONDecoder().decode([JsonFile.JsonBonuses].self, from: data)
print("loadBonusesFromFile successful")
return bonuses
} catch {
print("Error loading bonuses from file:", error)
return nil
}
}
func loadBonuses(completion: #escaping ([JsonFile.JsonBonuses]?) -> Void) {
let localBonusesURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("BonusData.json")
downloadJSON { bonuses in
if let bonuses = bonuses {
completion(bonuses)
self.saveBonuses(bonuses, to: localBonusesURL)
} else {
print("versions did not match")
completion(self.loadBonusesFromFile(localBonusesURL))
}
}
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredBonuses = bonuses.filter({( bonus: JsonFile.JsonBonuses) -> Bool in
return bonus.state.localizedCaseInsensitiveContains(searchText)
})
tableView.reloadData()
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? BonusDetailViewController {
destination.bonus = bonuses[(tableView.indexPathForSelectedRow?.row)!]
}
}
}
extension BonusListViewController: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
The part of the code that should be filtering the view is identical in both the cellForRowAt and the swipeAction methods:
let bonus: JsonFile.JsonBonuses
if self.isFiltering() {
bonus = self.filteredBonuses[indexPath.row]
} else {
bonus = self.bonuses[indexPath.row]
}
Originally the cellForRowAt did not have the self entries, but I added those to see if it would resolve it, and it didn't.
I found the cause of the issue. Near the bottom of the UITableViewController I had the following:
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? BonusDetailViewController {
destination.bonus = bonuses[(tableView.indexPathForSelectedRow?.row)!]
}
}
This is where the issue was. I fixed it by adding the same if/else logic I used in my cellForRowAt and SwipeAction:
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? BonusDetailViewController {
if self.isFiltering() {
destination.bonus = filteredBonuses[(tableView.indexPathForSelectedRow?.row)!]
} else {
destination.bonus = bonuses[(tableView.indexPathForSelectedRow?.row)!]
}
}
}

Getting an error when my app tries to download JSON

I finally started updating the BonusData.json file that my app uses. Now I am getting an error when it tries to load the data. The complete code is below but I am getting "JSON Download Failed", which is contained in the downloadJSON function.
If I'm reading my code right, that would mean that I'm encountering an error in the
let posts = try JSONDecoder().decode(JsonFile.self, from: data)
completed(posts.bonuses)
section, but I'm not sure how to troubleshoot that any further. What is supposed to be happening is that the app looks at the server, downloads the JSON and then saves it locally to be used to populate the UITableView. If there is no data connection, then it should not care, and just use the local saved version. Because the app is loading up blank, I'm assuming it is also not working as it is intended.
Here is the complete code:
import UIKit
import os.log
import Foundation
class BonusListViewController: UITableViewController {
var bonuses = [JsonFile.JsonBonuses]()
var filteredBonuses = [JsonFile.JsonBonuses]()
var detailViewController: BonusDetailViewController? = nil
let defaults = UserDefaults.standard
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
// MARK: Search Support
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Enter two letter state to filter"
navigationItem.searchController = searchController
definesPresentationContext = true
// MARK: Settings Data Struct
struct Constants {
struct RiderData {
let riderNumToH = "riderNumToH"
let pillionNumToH = "pillionNumToH"
}
struct RallyData {
let emailDestinationToH = "emailDestinationToH"
}
}
//MARK: Load the bonuses
print("About to call loadBonuses")
loadBonuses { [weak self] bonuses in
self?.bonuses = bonuses ?? []
DispatchQueue.main.async {
self?.tableView.reloadData()
}
print("loadBonuses called")
}
}
// MARK: - Table View Configuration
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
print("Showing \(filteredBonuses.count) Filtered Results")
return filteredBonuses.count
}
print("Found \(bonuses.count) rows in section.")
return bonuses.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "BonusListViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BonusListViewCell else {
fatalError("The dequeued cell is not an instance of BonusListViewCell.")
}
// let bonus = bonuses[indexPath.row]
let bonus: JsonFile.JsonBonuses
if isFiltering() {
bonus = filteredBonuses[indexPath.row]
} else {
bonus = bonuses[indexPath.row]
}
let urlString = "http://tourofhonor.com/appimages/"+(bonus.imageName)
let url = URL(string: urlString)
cell.primaryImage.downloadedFrom(url: url!)
cell.nameLabel.text = bonus.name.capitalized
cell.bonusCodeLabel.text = bonus.bonusCode.localizedUppercase
cell.categoryLabel.text = bonus.category
cell.valueLabel.text = "\(bonus.value)"
cell.cityLabel.text = "\(bonus.city.capitalized),"
cell.stateLabel.text = bonus.state.localizedUppercase
return cell
}
// MARK: Functions
// MARK: - Fetch JSON from ToH webserver
func downloadJSON(completed: #escaping ([JsonFile.JsonBonuses]?) -> ()) {
let url = URL(string: "http://tourofhonor.com/BonusData.json")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error == nil, let data = data {
do {
let posts = try JSONDecoder().decode(JsonFile.self, from: data)
completed(posts.bonuses)
print("URLSession did not fail")
} catch {
print("JSON Download Failed")
}
} else {
print("downloadJSON completed")
completed(nil)
}
}.resume()
}
func saveBonuses(_ bonuses: [JsonFile.JsonBonuses], to url: URL) {
try? FileManager.default.removeItem(at: url)
do {
let data = try JSONEncoder().encode(bonuses)
try data.write(to: url)
print("saveBonuses successful")
} catch {
print("Error saving bonuses to file:", error)
}
}
func loadBonusesFromFile(_ url: URL) -> [JsonFile.JsonBonuses]? {
do {
let data = try Data(contentsOf: url)
let bonuses = try JSONDecoder().decode([JsonFile.JsonBonuses].self, from: data)
print("loadBonusesFromFile successful")
return bonuses
} catch {
print("Error loading bonuses from file:", error)
return nil
}
}
func loadBonuses(completion: #escaping ([JsonFile.JsonBonuses]?) -> Void) {
let localBonusesURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("BonusData.json")
downloadJSON { bonuses in
if let bonuses = bonuses {
completion(bonuses)
self.saveBonuses(bonuses, to: localBonusesURL)
} else {
print("versions did not match")
completion(self.loadBonusesFromFile(localBonusesURL))
}
}
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredBonuses = bonuses.filter({( bonus: JsonFile.JsonBonuses) -> Bool in
return bonus.state.localizedCaseInsensitiveContains(searchText)
})
tableView.reloadData()
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? BonusDetailViewController {
destination.bonus = bonuses[(tableView.indexPathForSelectedRow?.row)!]
}
}
}
extension BonusListViewController: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
The JSON is hosted here: http://tourofhonor.com/BonusData.json
It looks like the JSON you're trying to download is not formatted correctly. It's missing a comma between objects, and it has an extra comma at the end of the list.
There are a number of tools to validate JSON, but one accessible one is https://jsonlint.com/. If you paste the output from http://tourofhonor.com/BonusData.json there, it will highlight the formatting errors for you and give you some guidance on how to fix them.
I'm going to focus on what I think is the core of your question rather than the technical fix.
I'm not sure how to troubleshoot that any further.
do {
// ...
let posts = try JSONDecoder().decode(JsonFile.self, from: data)
// ...
} catch let error {
// Do something with this error.
}
decode throws details about the exception which you can do when you get the error.

UISearchBar seems to be returning right count, but not the right rows

I am trying to implement a search function in my app. For now, I'm just trying to search by the State value my JSON, though I'd like to eventually include Category as well. There are 9 rows total, the first 7 are State=AZ and the last 2 are State=CA. When I search for "KK" the table is empty, which makes sense. But when I search for "CA" I get two rows like I expect, but they are the first two rows in the JSON, which are both AZ, not the two CA rows it should be.
I suspect my issue is somewhere in my filterContentForSearchText function, but since I'm not sure exactly which code you need, here is the ViewController (the function I think is the issue is down near the end):
import UIKit
import os.log
import Foundation
class BonusListViewController: UITableViewController {
var bonuses = [JsonFile.JsonBonuses]()
var filteredBonuses = [JsonFile.JsonBonuses]()
var detailViewController: BonusDetailViewController? = nil
let defaults = UserDefaults.standard
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
// MARK: Search Support
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Enter two letter state to filter"
navigationItem.searchController = searchController
definesPresentationContext = true
// MARK: Settings Data Struct
struct Constants {
struct RiderData {
let riderNumToH = "riderNumToH"
let pillionNumToH = "pillionNumToH"
}
struct RallyData {
let emailDestinationToH = "emailDestinationToH"
}
}
//MARK: Load the bonuses
loadBonuses { [weak self] bonuses in
self?.bonuses = bonuses ?? []
DispatchQueue.main.async {
self?.tableView.reloadData()
}
print("loadBonuses called")
}
}
// MARK: - Table View Configuration
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
print("Showing \(filteredBonuses.count) Filtered Results")
return filteredBonuses.count
}
print("Found \(bonuses.count) rows in section.")
return bonuses.count
}
/* Disabling the swipe function until I code it to actually do something
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let clearAction = UIContextualAction(style: .normal, title: "Clear Data") { (contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in
print("Clear Action Tapped")
completionHandler(true)
}
clearAction.backgroundColor = .blue
let swipeConfig = UISwipeActionsConfiguration(actions: [clearAction])
return swipeConfig
}
*/
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "BonusListViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BonusListViewCell else {
fatalError("The dequeued cell is not an instance of BonusListViewCell.")
}
let bonus = bonuses[indexPath.row]
let bonusSet: JsonFile.JsonBonuses
if isFiltering() {
bonusSet = filteredBonuses[indexPath.row]
} else {
bonusSet = bonus
}
let urlString = "http://tourofhonor.com/appimages/"+(bonus.imageName)
let url = URL(string: urlString)
cell.primaryImage.downloadedFrom(url: url!)
cell.nameLabel.text = bonus.name.capitalized
cell.bonusCodeLabel.text = bonus.bonusCode.localizedUppercase
cell.categoryLabel.text = bonus.category
cell.valueLabel.text = "\(bonus.value)"
cell.cityLabel.text = "\(bonus.city.capitalized),"
cell.stateLabel.text = bonus.state.localizedUppercase
return cell
}
// MARK: Functions
// MARK: - Fetch JSON from ToH webserver
func downloadJSON(completed: #escaping ([JsonFile.JsonBonuses]?) -> ()) {
let url = URL(string: "http://tourofhonor.com/BonusData.json")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error == nil, let data = data {
do {
let posts = try JSONDecoder().decode(JsonFile.self, from: data)
completed(posts.bonuses)
} catch {
print("JSON Download Failed")
}
} else {
print("downloadJSON completed")
completed(nil)
}
}.resume()
}
func saveBonuses(_ bonuses: [JsonFile.JsonBonuses], to url: URL) {
try? FileManager.default.removeItem(at: url)
do {
let data = try JSONEncoder().encode(bonuses)
try data.write(to: url)
print("saveBonuses successful")
} catch {
print("Error saving bonuses to file:", error)
}
}
func loadBonusesFromFile(_ url: URL) -> [JsonFile.JsonBonuses]? {
do {
let data = try Data(contentsOf: url)
let bonuses = try JSONDecoder().decode([JsonFile.JsonBonuses].self, from: data)
print("loadBonusesFromFile successful")
return bonuses
} catch {
print("Error loading bonuses from file:", error)
return nil
}
}
func loadBonuses(completion: #escaping ([JsonFile.JsonBonuses]?) -> Void) {
let localBonusesURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Bonuses.json")
downloadJSON { bonuses in
if let bonuses = bonuses {
completion(bonuses)
self.saveBonuses(bonuses, to: localBonusesURL)
} else {
completion(self.loadBonusesFromFile(localBonusesURL))
}
}
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredBonuses = bonuses.filter({( bonus: JsonFile.JsonBonuses) -> Bool in
return bonus.state.lowercased().contains(searchText.lowercased())
})
tableView.reloadData()
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? BonusDetailViewController {
destination.bonus = bonuses[(tableView.indexPathForSelectedRow?.row)!]
}
}
}
extension BonusListViewController: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
and here is the JsonFile.swift:
import Foundation
struct JsonFile: Codable {
struct Meta: Codable {
let fileName: String
let version: String
}
struct JsonBonuses: Codable {
let bonusCode: String
let category: String
let name: String
let value: Int
let city: String
let state: String
let flavor: String
let imageName: String
}
let meta: Meta
let bonuses: [JsonBonuses]
}
EDIT: The JSON itself can be found at http://www.tourofhonor.com/BonusData.json
Also, on the line that says let bonusSet: JsonFile.JsonBonuses (under the cellForRowAt), I'm getting a warning that says "Immutable value bonusSet was never used; consider removing it" even though I use it in the very next line.
I guess the issue is in your cellForRow method, you are supposed to assignv alues with bonusSet and not bonus. as you are initializing the value from bonus dara structure which should be from bonusSet.
Try changing cellForRow as:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "BonusListViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BonusListViewCell else {
fatalError("The dequeued cell is not an instance of BonusListViewCell.")
}
let bonus = bonuses[indexPath.row]
let bonusSet: JsonFile.JsonBonuses
if isFiltering() {
bonusSet = filteredBonuses[indexPath.row]
} else {
bonusSet = bonus
}
//CHANGE IS REQUIRED HERE: REPLACE THE bonus WITH bonusSet :
let urlString = "http://tourofhonor.com/appimages/"+(bonusSet.imageName)
let url = URL(string: urlString)
cell.primaryImage.downloadedFrom(url: url!)
cell.nameLabel.text = bonusSet.name.capitalized
cell.bonusCodeLabel.text = bonusSet.bonusCode.localizedUppercase
cell.categoryLabel.text = bonusSet.category
cell.valueLabel.text = "\(bonusSet.value)"
cell.cityLabel.text = "\(bonusSet.city.capitalized),"
cell.stateLabel.text = bonusSet.state.localizedUppercase
return cell
}
The problem is with your cell for row index path
search result you are getting from the filterContentForSearchText you are storing in filteredBonuses but in cellForRowAt you are still setting all your values from
bouns variable
bonus = bonuses[indexPath.row]
if isFiltering() {
bonusSet = filteredBonuses[indexPath.row] //even though you are creating bonusSet you are not using it while setting cell values below so use that bonusSet
} else {
bonusSet = bonus
}
//Like this
let urlString = "http://tourofhonor.com/appimages/"+(bonusSet.imageName)
let url = URL(string: urlString)
cell.primaryImage.downloadedFrom(url: url!)
cell.nameLabel.text = bonus.name.capitalized
cell.bonusCodeLabel.text = bonusSet.bonusCode.localizedUppercase
cell.categoryLabel.text = bonusSet.category
cell.valueLabel.text = "\(bonusSet.value)"
cell.cityLabel.text = "\(bonusSet.city.capitalized),"
cell.stateLabel.text = bonusSet.state.localizedUppercase
This code is useless:
let bonusSet: JsonFile.JsonBonuses
if isFiltering() {
bonusSet = filteredBonuses[indexPath.row]
} else {
bonusSet = bonus
}
You create a local variable bonusSet whose value depends on whether you are filtering; but, as the compiler rightly observes, nothing you do afterwards uses it. Your code thus behaves exactly the same way regardless of whether you are filtering.

Resources