I have a tableview in my view controller and i am trying to implement the pull to refresh function. When i pull to refresh the table view does not reload and the data is not updated.
When i print the array print(tempRefresh) inside of my fetchRefresh function, it returns an empty array??
my firebase database looks like this.
override func viewDidLoad() {
super.viewDidLoad()
// refresh the page
refreshControl = UIRefreshControl()
if #available(iOS 10.0, *) {
tableView.refreshControl = refreshControl
} else {
// fallback on older versions
tableView.addSubview(refreshControl)
}
refreshControl.attributedTitle = NSAttributedString(string: "Loading...")
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
// reload the tabelview
tableView.reloadData()
tableView.delegate = self
tableView.dataSource = self
}
#objc func handleRefresh() {
print("refresh")
beginRefresh()
DispatchQueue.main.async {
self.refreshControl.endRefreshing()
}
}
func fetchRefresh(completion: #escaping(_ comments:[Comments])->()) {
oldCommentsQuery.queryLimited(toLast: 20).observeSingleEvent(of: .value, with: { snapshot in
var tempRefresh = [Comments]()
let commentsSnap = snapshot.childSnapshot(forPath: "comments")
let allComments = commentsSnap.children.allObjects as! [DataSnapshot]
for commentSnap in allComments {
let degree = commentSnap.childSnapshot(forPath: "reply degree").value as? String ?? ""
let name = commentSnap.childSnapshot(forPath: "reply name").value as? String ?? ""
let text = commentSnap.childSnapshot(forPath: "reply text").value as? String ?? ""
let university = commentSnap.childSnapshot(forPath: "reply university").value as? String ?? ""
let photoURL = commentSnap.childSnapshot(forPath: "reply url").value as? String ?? ""
let url = URL(string: photoURL)
let timestamp = commentSnap.childSnapshot(forPath: "timestamp").value as? Double
let lastComment = self.comments.last
if snapshot.key != lastComment?.id {
let newRefresh = Comments(id: snapshot.key, fullname: name, commentText: text, university: university, degree: degree, photoURL: photoURL, url: url!, timestamp: timestamp!)
tempRefresh.insert(newRefresh, at: 0)
//test
print(tempRefresh)
}
}
return completion(tempRefresh)
})
}
func beginRefresh() {
fetchingMore = true
// fetch the comments
fetchRefresh { newComments in
self.comments.append(contentsOf: newComments)
self.endReached = newComments.count == 0
self.fetchingMore = false
self.tableView.reloadData()
}
}
I have a page based app, using RootViewController, ModelViewController, DataViewController, and a SearchViewController.
In my searchViewController, I search for an item and then add or remove that Item to an array which is contained in a Manager class(and UserDefaults), which the modelViewController uses to instantiate an instance of DataViewController with the correct information loaded using the dataObject. Depending on whether an Item was added or removed, I use a Bool to determine which segue was used, addCoin or removeCoin, so that the RootViewController(PageView) will show either the last page in the array, (when a page is added) or the first (when removed).
Everything was working fine until I ran into an error which I can not diagnose, the problem is that when I add a page, the app crashes, giving me a "unexpectadely found nil when unwrapping an optional value"
This appears to be the problem function, in the searchViewController 'self.performSegue(withIdentifier: "addCoin"' seems to be called instantly, even without the dispatchque:
#objc func addButtonAction(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.addCoin(coin: dataObject)
CGPrices.shared.getData(arr: true, completion: { (success) in
print(Manager.shared.coins)
DispatchQueue.main.async {
self.performSegue(withIdentifier: "addCoin", sender: self)
}
})
}
searchBar.text = ""
}
Meaning that In my DataViewController, this function will find nil:
func getIndex() {
let index = CGPrices.shared.coinData.index(where: { $0.id == dataObject })!
dataIndex = index
}
I can't find out why it does not wait for completion.
I also get this error about threads:
[Assert] Cannot be called with asCopy = NO on non-main thread.
which is why I try to do the push segue using dispatch que
Here is my searchViewController full code:
import UIKit
class SearchViewController: UIViewController, UISearchBarDelegate {
let selectionLabel = UILabel()
let searchBar = UISearchBar()
let addButton = UIButton()
let removeButton = UIButton()
var filteredObject: [String] = []
var dataObject = ""
var isSearching = false
//Add Button Action.
#objc func addButtonAction(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.addCoin(coin: dataObject)
CGPrices.shared.getData(arr: true, completion: { (success) in
print(Manager.shared.coins)
DispatchQueue.main.async {
self.performSegue(withIdentifier: "addCoin", sender: self)
}
})
}
searchBar.text = ""
}
//Remove button action.
#objc func removeButtonActon(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.removeCoin(coin: dataObject)
self.performSegue(withIdentifier: "addCoin", sender: self)
}
searchBar.text = ""
}
//Prepare for segue, pass removeCoinSegue Bool depending on remove or addCoin.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "addCoin" {
if let destinationVC = segue.destination as? RootViewController {
destinationVC.addCoinSegue = true
}
} else if segue.identifier == "addCoin" {
if let destinationVC = segue.destination as? RootViewController {
destinationVC.addCoinSegue = false
}
}
}
//Remove button action.
#objc func removeButtonAction(sender: UIButton!) {
if Manager.shared.coins.count == 1 {
removeAlert()
} else {
Manager.shared.removeCoin(coin: dataObject)
print(Manager.shared.coins)
print(dataObject)
searchBar.text = ""
self.removeButton.isHidden = true
DispatchQueue.main.async {
self.performSegue(withIdentifier: "removeCoin", sender: self)
}
}
}
//Search/Filter the struct from CGNames, display both the Symbol and the Name but use the ID as dataObject.
func filterStructForSearchText(searchText: String, scope: String = "All") {
if !searchText.isEmpty {
isSearching = true
filteredObject = CGNames.shared.coinNameData.filter {
// if you need to search key and value and include partial matches
// $0.key.contains(searchText) || $0.value.contains(searchText)
// if you need to search caseInsensitively key and value and include partial matches
$0.name.range(of: searchText, options: .caseInsensitive) != nil || $0.symbol.range(of: searchText, options: .caseInsensitive) != nil
}
.map{ $0.id }
} else {
isSearching = false
print("NoText")
}
}
//Running filter function when text changes.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filterStructForSearchText(searchText: searchText)
if isSearching == true && filteredObject.count > 0 {
addButton.isHidden = false
dataObject = filteredObject[0]
selectionLabel.text = dataObject
if Manager.shared.coins.contains(dataObject) {
removeButton.isHidden = false
addButton.isHidden = true
} else {
removeButton.isHidden = true
addButton.isHidden = false
}
} else {
addButton.isHidden = true
removeButton.isHidden = true
selectionLabel.text = "e.g. btc/bitcoin"
}
}
override func viewDidLoad() {
super.viewDidLoad()
//Setup the UI.
self.view.backgroundColor = .gray
setupView()
}
override func viewDidLayoutSubviews() {
}
//Hide keyboard
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
//Alerts
func removeAlert() {
let alertController = UIAlertController(title: "Can't Remove", message: "\(dataObject) can't be deleted, add another to delete \(dataObject)", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
func Duplicate() {
let alertController = UIAlertController(title: "Duplicate", message: "\(dataObject) is already in your pages!", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
func max() {
let alertController = UIAlertController(title: "Maximum Reached", message: "\(dataObject) can't be added, you have reached the maximum of 5 coins. Please delete a coin to add another.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
}
and here is the DataViewController
import UIKit
class DataViewController: UIViewController {
#IBOutlet weak var dataLabel: UILabel!
//Variables and Objects.
//The dataObject carries the chosen cryptocurrencies ID from the CoinGecko API to use to get the correct data to load on each object.
var dataObject = String()
//The DefaultCurrency (gbp, eur...) chosen by the user.
var defaultCurrency = ""
//The Currency Unit taken from the exchange section of the API.
var currencyUnit = CGExchange.shared.exchangeData[0].rates.gbp.unit
var secondaryUnit = CGExchange.shared.exchangeData[0].rates.eur.unit
var tertiaryUnit = CGExchange.shared.exchangeData[0].rates.usd.unit
//Index of the dataObject
var dataIndex = Int()
//Objects
let cryptoLabel = UILabel()
let cryptoIconImage = UIImageView()
let secondaryPriceLabel = UILabel()
let mainPriceLabel = UILabel()
let tertiaryPriceLabel = UILabel()
//Custom Fonts.
let customFont = UIFont(name: "AvenirNext-Heavy", size: UIFont.labelFontSize)
let secondFont = UIFont(name: "AvenirNext-BoldItalic" , size: UIFont.labelFontSize)
//Setup Functions
//Get the index of the dataObject
func getIndex() {
let index = CGPrices.shared.coinData.index(where: { $0.id == dataObject })!
dataIndex = index
}
//Label
func setupLabels() {
//cryptoLabel from dataObject as name.
cryptoLabel.text = CGPrices.shared.coinData[dataIndex].name
//Prices from btc Exchange rate.
let btcPrice = CGPrices.shared.coinData[dataIndex].current_price!
let dcExchangeRate = CGExchange.shared.exchangeData[0].rates.gbp.value
let secondaryExchangeRate = CGExchange.shared.exchangeData[0].rates.eur.value
let tertiaryExchangeRate = CGExchange.shared.exchangeData[0].rates.usd.value
let realPrice = (btcPrice * dcExchangeRate)
let secondaryPrice = (btcPrice * secondaryExchangeRate)
let tertiaryPrice = (btcPrice * tertiaryExchangeRate)
secondaryPriceLabel.text = "\(secondaryUnit)\(String((round(1000 * secondaryPrice) / 1000)))"
mainPriceLabel.text = "\(currencyUnit)\(String((round(1000 * realPrice) /1000)))"
tertiaryPriceLabel.text = "\(tertiaryUnit)\(String((round(1000 * tertiaryPrice) / 1000)))"
}
//Image
func getIcon() {
let chosenImage = CGPrices.shared.coinData[dataIndex].image
let remoteImageUrl = URL(string: chosenImage)
guard let url = remoteImageUrl else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
DispatchQueue.main.async {
self.cryptoIconImage.image = UIImage(data: data)
}
}
}.resume()
}
override func viewDidLoad() {
super.viewDidLoad()
// for family in UIFont.familyNames.sorted() {
// let names = UIFont.fontNames(forFamilyName: family)
// print("Family: \(family) Font names: \(names)")
// }
// Do any additional setup after loading the view, typically from a nib.
self.setupLayout()
self.getIndex()
self.setupLabels()
self.getIcon()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.dataLabel!.text = dataObject
view.backgroundColor = .lightGray
}
}
Edit: CGPrices Class with getData method:
import Foundation
class CGPrices {
struct Coins: Decodable {
let id: String
let name: String
let symbol: String
let image: String
let current_price: Double?
let low_24h: Double?
//let price_change_24h: Double?
}
var coinData = [Coins]()
var defaultCurrency = ""
var coins = Manager.shared.coins
var coinsEncoded = ""
static let shared = CGPrices()
func encode() {
for i in 0..<coins.count {
coinsEncoded += coins[i]
if (i + 1) < coins.count { coinsEncoded += "%2C" }
}
print("encoded")
}
func getData(arr: Bool, completion: #escaping (Bool) -> ()) {
encode()
let urlJSON = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=\(coinsEncoded)"
guard let url = URL(string: urlJSON) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let coinsData = try JSONDecoder().decode([Coins].self, from: data)
self.coinData = coinsData
completion(arr)
} catch let jsonErr {
print("error serializing json: \(jsonErr)")
print(data)
}
}.resume()
}
func refresh(completion: () -> ()) {
defaultCurrency = UserDefaults.standard.string(forKey: "DefaultCurrency")!
completion()
}
}
I figured it out.
The problem was inside my getData method I was not updated the coins array:
var coinData = [Coins]()
var defaultCurrency = ""
var coins = Manager.shared.coins
var coinsEncoded = ""
static let shared = CGPrices()
func encode() {
for i in 0..<coins.count {
coinsEncoded += coins[i]
if (i+1)<coins.count { coinsEncoded+="%2C" }
}
print("encoded")
}
I needed to add this line in getData:
func getData(arr: Bool, completion: #escaping (Bool) -> ()) {
//Adding this line to update the array so that the URL is appended correctly.
coins = Manager.shared.coins
encode()
let urlJSON = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=\(coinsEncoded)"
This would fix the finding nil in the DataViewController, but the app would still crash do to updating UI Elements on a background thread, as the segue was called inside the completion handler of the getData method. to fix this, I used DispatchQue.Main.Async on the segue inside the getData method in the addButton function, to ensure that everything is updated on the main thread, like so:
#objc func addButtonAction(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.addCoin(coin: dataObject)
print("starting")
CGPrices.shared.getData(arr: true) { (arr) in
print("complete")
print(CGPrices.shared.coinData)
//Here making sure it is updated on main thread.
DispatchQueue.main.async {
self.performSegue(withIdentifier: "addCoin", sender: self)
}
}
}
searchBar.text = ""
}
Thanks for all the comments as they helped me to figure this out, and I learned a lot in doing so. Hopefully this can help someone else in their thought process when debugging, as one can get so caught up in one area of a problem, and forget to take a step back and look to other areas.
I have an edit button (revealed when user swipes right) that I'm trying to link to the below table view but with the user information already in the fields. How would I go about doing something like that?
I imagine it has something to do with this piece but everything I have tried isn't working:
let newRelease = Release_Date(context: context)
newRelease.artist = artist
newRelease.album = album
newRelease.release_date = releasedate as NSDate?
newRelease.release_dateId = UUID().uuidString
Below is the table view code:
import UIKit
import CoreData
import UserNotifications
class AddfreshreleaseViewController: UIViewController {
#IBOutlet var artisttextfield: UITextField!
#IBOutlet var albumtextfield: UITextField!
#IBOutlet var releasedatePicker: UIDatePicker!
override func viewDidLoad() {
super.viewDidLoad()
releasedatePicker.minimumDate = Date()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func saveTapped( _ sender: UIBarButtonItem) {
let artist = artisttextfield.text ?? ""
let album = albumtextfield.text ?? ""
let releasedate = releasedatePicker.date
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let newRelease = Release_Date(context: context)
newRelease.artist = artist
newRelease.album = album
newRelease.release_date = releasedate as NSDate?
newRelease.release_dateId = UUID().uuidString
if let uniqueId = newRelease.release_dateId {
print("The freshreleaseId is \(uniqueId)")
}
do {
try context.save()
let message = "\(artist)'s new album \(album) releases Today!"
let content = UNMutableNotificationContent()
content.body = message
content.sound = UNNotificationSound.default()
var dateComponents = Calendar.current.dateComponents([.month, .day],
from: releasedate)
dateComponents.hour = 09
dateComponents.minute = 00
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents,
repeats: true)
if let identifier = newRelease.release_dateId {
let request = UNNotificationRequest(identifier: identifier,
content: content, trigger: trigger)
let center = UNUserNotificationCenter.current()
center.add(request, withCompletionHandler: nil)
}
} catch let error {
print("Could not save because of \(error).")
}
dismiss(animated: true, completion: nil)
print("Added a Release Date!")
print("Artist: \(newRelease.artist)")
print("Album: \(newRelease.album)")
print("Release Date: \(newRelease.release_date)")
}
#IBAction func cancelTapped(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
}
I'm new to the coding world. I'm trying to incorporate an edit button. I got the button to show up while the user swipes to the right but I'm having trouble getting the edit button to work. When pressed, it should bring up the table were the user first input the data but with the information already placed in the fields.
For example: user fills in the artist, album and release date, the edit button should take them back to where they first put in the information but with the info already in place.
Here's the code I used for the edit button:
override func tableView(_ tableView: UITableView,
leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let modifyAction = UIContextualAction(style: .normal, title: "Update", handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in
print("Update action ...")
success(true)
})
modifyAction.title = "Edit"
modifyAction.backgroundColor = .blue
return UISwipeActionsConfiguration(actions: [modifyAction])
}
And here's the code for adding the content:
#IBOutlet var artisttextfield: UITextField!
#IBOutlet var albumtextfield: UITextField!
#IBOutlet var releasedatePicker: UIDatePicker!
override func viewDidLoad() {
super.viewDidLoad()
releasedatePicker.minimumDate = Date()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func saveTapped( _ sender: UIBarButtonItem) {
let artist = artisttextfield.text ?? ""
let album = albumtextfield.text ?? ""
let releasedate = releasedatePicker.date
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let newRelease = Release_Date(context: context)
newRelease.artist = artist
newRelease.album = album
newRelease.release_date = releasedate as NSDate?
newRelease.release_dateId = UUID().uuidString
if let uniqueId = newRelease.release_dateId {
print("The freshreleaseId is \(uniqueId)")
}
do {
try context.save()
let message = "\(artist)'s new album \(album) releases Today!"
let content = UNMutableNotificationContent()
content.body = message
content.sound = UNNotificationSound.default()
var dateComponents = Calendar.current.dateComponents([.month, .day],
from: releasedate)
dateComponents.hour = 09
dateComponents.minute = 00
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents,
repeats: true)
if let identifier = newRelease.release_dateId {
let request = UNNotificationRequest(identifier: identifier,
content: content, trigger: trigger)
let center = UNUserNotificationCenter.current()
center.add(request, withCompletionHandler: nil)
}
} catch let error {
print("Could not save because of \(error).")
}
dismiss(animated: true, completion: nil)
print("Added a Release Date!")
print("Artist: \(newRelease.artist)")
print("Album: \(newRelease.album)")
print("Release Date: \(newRelease.release_date)")
}
#IBAction func cancelTapped(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
Here's viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest = Release_Date.fetchRequest() as NSFetchRequest<Release_Date>
let sortDescriptor1 = NSSortDescriptor(key: "album", ascending: true)
let sortDescriptor2 = NSSortDescriptor(key: "artist", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]
do {
freshreleases = try context.fetch(fetchRequest)
} catch let error {
print("Could not fetch because of error: \(error).")
}
tableView.reloadData()
}
I am trying to send data to another view controller. However, the data cannot be reached at the second view controller. Here is my code:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
switch(segue.identifier ?? "") {
case "tograddetail":
print("Going to Grad Detail")
guard let gradDetailViewController = segue.destination as? graduatedetailViewController else {
fatalError("Unexpected destination: \(segue.destination)")
}
guard let selectedgradCell = sender as? GradTableViewCell else {
fatalError("Unexpected sender: \(sender)")
}
guard let indexPath = tableView.indexPath(for: selectedgradCell) else {
fatalError("The selected cell is not being displayed by the table")
}
ref = FIRDatabase.database().reference().child("Database")
ref.observe(FIRDataEventType.value, with: { (snapshot) in
//print(snapshot.value)
if snapshot.exists() {
if let countdowntime = snapshot.value as? NSDictionary {
let selectedgrad = self.graduatename[indexPath.row]
if let graddata = countdowntime[selectedgrad] as? NSDictionary {
let theinstitution = graddata["Institution"] as! String
let thelocation = graddata["location"] as! String
let thetimeleft = graddata["timeleft"] as! Int
guard let firstgrad = graddetail(institution: theinstitution, location: thelocation, timeleft: thetimeleft) else {
fatalError("Unable to instantiate graddetail")
}
//print(firstgrad.institution)
//print(destinationgraddata.grad?.institution)
let destinationVC = segue.destination as! graduatedetailViewController
destinationVC.grad = firstgrad
}
}
}
})
default:
fatalError("Unexpected Segue Identifier; \(segue.identifier)")
}
}
And here is my code for the second view controller:
var grad: graddetail?
#IBOutlet weak var theinstitution: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
if let grad = grad {
theinstitution.text = grad.institution
}
}
However, the grad.institution value always return nil. Any idea?
The issue is observe(_:with:) is async and segue will called synchronously, so that when you get response in completion block of observe your segue is already performed.
To solved the issue what you need to do is call the observe before calling the performSegue and inside the completion block of observe when you get response call the perfromSegue with the value that you want to pass.