You get one thing figured out and something else comes along and that's why I love this stuff.
My workout app uses realm for data persistence and when I modify some of the data elements of the file, it crashes when I reload the tableView. Here is a layout of the tableview in WorkoutViewController.
I select an item to edit it's three properties (Name, Date and average time). When selecting Edit the following view controller is presented. This is WorkoutAlertViewController.
Here is the relevant code:
class WorkoutViewController: UIViewController {
let realm = try! Realm()
var workoutItems: Results<Workout>?
var passedWorkout = Workout()
#IBOutlet weak var tableView: UITableView!
func editWorkout(workout: Workout, name: String, time: String, date: Date) {
do {
try self.realm.write {
passedWorkout = workout
print("passedWorkout \(workout)")
print("changes to be made \(name), \(time), \(date)")
passedWorkout.name = name
passedWorkout.time = time
passedWorkout.dateCreated = date
print("workout changed \(passedWorkout)")
}
} catch {
print("Error editing workout \(error)")
}
// loadWorkout()
}
func loadWorkout() {
workoutItems = realm.objects(Workout.self).sorted(byKeyPath: "name", ascending: false)
print("workoutItems \(String(describing: workoutItems))")
tableView.reloadData()
}
}
extension WorkoutViewController: PassNewWorkout {
func didTapAdd(name: String, time: String, date: Date) {
workoutName = name
averageTime = time
creationDate = date
let newWorkout = Workout()
newWorkout.name = workoutName
newWorkout.dateCreated = creationDate
newWorkout.time = averageTime
saveWorkout(workout: newWorkout)
}
}
extension WorkoutViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return workoutItems?.count ?? 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! WorkoutCell
if let workout = workoutItems?[indexPath.row] {
cell.workoutLabel.text = workout.name
cell.dateLabel.text = String("Created: \(functions.convertTime(timeToConvert: workout.dateCreated!))")
cell.timeLabel.text = String("Avg Time: \(workout.time)")
} else {
cell.textLabel?.text = "No Workouts Added"
}
return cell
}
}
extension WorkoutViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "workoutListSegue", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "workoutListSegue" {
if let destinationVC = segue.destination as? WorkoutListViewController, let indexPath = tableView.indexPathForSelectedRow {
if let workout = workoutItems?[indexPath.row] {
destinationVC.selectedWorkout = workout
destinationVC.workoutName = workout.name
}
}
}
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { (action, view, actionPerformed) in
if let _ = tableView.cellForRow(at: indexPath){
self.passedWorkout = self.workoutItems![indexPath.row]
self.deleteWorkout(workout: self.passedWorkout)
}
tableView.reloadData()
}
deleteAction.backgroundColor = .red
let editAction = UIContextualAction(style: .normal, title: "Edit") { (action, view, actionPerformed) in
if let _ = tableView.cellForRow(at: indexPath){
let alertVC = self.storyboard!.instantiateViewController(identifier: "WorkoutVC") as! WorkoutAlertViewController
self.passedWorkout = self.workoutItems![indexPath.row]
print("editAction \(self.passedWorkout)")
alertVC.didTapEdit(workout: self.passedWorkout)
self.present(alertVC, animated: true, completion: nil)
}
}
editAction.backgroundColor = .gray
return UISwipeActionsConfiguration(actions: [deleteAction, editAction])
}
}
The app crashes on loadWorkout() with:
Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value: file /Users/kevin/Desktop/FitTrax/FitTrax/Controller/Fitness/WorkoutViewController.swift, line 88
Line 88 is tableView.reloadData()
Here is the WorkoutAlertViewController file.
protocol PassNewWorkout {
func didTapAdd(name: String, time: String, date: Date)
}
class WorkoutAlertViewController: UIViewController {
var workoutName = String()
var averageTime = String()
var creationDate = Date()
var receivedWorkout = Workout()
#IBAction func addButtonPressed(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
if workoutName == "" {
workoutName = workoutTextField.text!
creationDate = datePicker.date
averageTime = timeTextField.text!
alertDelegate.didTapAdd(name: workoutName, time: averageTime, date: creationDate)
} else {
workoutName = workoutTextField.text!
creationDate = datePicker.date
averageTime = timeTextField.text!
let workoutVC = storyboard!.instantiateViewController(withIdentifier: "Main") as! WorkoutViewController
workoutVC.editWorkout(workout: receivedWorkout, name: workoutName, time: averageTime, date: creationDate)
}
}
func didTapEdit(workout: Workout) {
workoutName = workout.name
averageTime = workout.time
creationDate = workout.dateCreated!
receivedWorkout = workout
}
}
The edit does work without the reload but does not immediately show the changes. If I go back to the Fitness Menu and back to the Workout Menu, it displays the changes then. It's something with that reload that I can't figure out.
Thank you for any suggestions.
Related
I am trying to get the inputted text from recipeName, servingsNumber, prepTime and cookTime to save to fb.
I know this is close to where I need to be. I am need some help with my save function. I need to assign the textField.text in sendToFirebase() I am able to print out the inputted text into the console and I was able to get it to save a document to FB but the entry was null. What am I missing? I feel like it is here in my code:
import UIKit
import Firebase
class AddRecipeViewController: UIViewController {
#IBOutlet weak var table: UITableView!
#IBOutlet weak var saveButton: UIBarButtonItem!
let db = Firestore.firestore()
var id = UUID().uuidString
var data = [RecipeData]()
let user = Auth.auth().currentUser?.email
var objRecipe = RecipeData.init()
override func viewDidLoad() {
super.viewDidLoad()
table.delegate = self
table.dataSource = self
navigationItem.hidesBackButton = true
let newBackButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelPressed))
self.navigationItem.leftBarButtonItem = newBackButton
table.register(UINib(nibName: "RecipeNameCell", bundle: nil), forCellReuseIdentifier: "recipeName")
table.register(UINib(nibName: "ServingSizeTableViewCell", bundle: nil), forCellReuseIdentifier: "servings")
table.register(UINib(nibName: "PrepTimeTableViewCell", bundle: nil), forCellReuseIdentifier: "prep")
table.register(UINib(nibName: "CookTimeTableViewCell", bundle: nil), forCellReuseIdentifier: "cook")
}
func sendToFirebase() {
do {
let recipeName = objRecipe.recipeName
let servings = objRecipe.servingsNumber
let prep = objRecipe.prepTime
let cook = objRecipe.cookTime
let user = Auth.auth().currentUser?.email
let id = objRecipe.id
let data = try JSONEncoder().encode(objRecipe)
let dictionary = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
db.collection("Recipe").document(objRecipe.id).setData(dictionary)
}
catch {
print(error)
}
}
#objc func cancelPressed(sender: UIBarButtonItem) {
navigationController?.popViewController(animated: true)
dismiss(animated: true, completion: nil)
}
#IBAction func savePressed(_ sender: UIBarButtonItem) {
sendToFirebase()
}
}
extension AddRecipeViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = table.dequeueReusableCell(withIdentifier: "recipeName", for: indexPath) as! RecipeNameCell
cell.textField.delegate = self
cell.textField.tag = indexPath.row
return cell
}
else if indexPath.row == 1 {
let cell = table.dequeueReusableCell(withIdentifier: "servings", for: indexPath) as! ServingSizeTableViewCell
cell.textField.delegate = self
cell.textField.tag = indexPath.row
return cell
}
else if indexPath.row == 2 {
let cell = table.dequeueReusableCell(withIdentifier: "prep", for: indexPath) as! PrepTimeTableViewCell
cell.textField.delegate = self
cell.textField.tag = indexPath.row
return cell
}
else if indexPath.row == 3{
let cell = table.dequeueReusableCell(withIdentifier: "cook", for: indexPath) as! CookTimeTableViewCell
cell.textField.delegate = self
cell.textField.tag = indexPath.row
return cell
}
else {
let cell = table.dequeueReusableCell(withIdentifier: "recipeName", for: indexPath) as! RecipeNameCell
return cell
}
}
}
extension AddRecipeViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
switch textField.tag {
case 0:
let recipeName = textField.text!
print(recipeName)
objRecipe.recipeName = textField.text!
// sendToFirebase() // Send to firebase
case 1:
let servingSize = textField.text!
print(servingSize)
objRecipe.servingsNumber = textField.text!
// sendToFirebase() // Send to firebase
case 2:
let prepTime = textField.text!
print(prepTime)
objRecipe.prepTime = textField.text!
// sendToFirebase() // Send to firebase
case 3:
let cookTime = textField.text!
print(cookTime)
objRecipe.cookTime = textField.text!
// sendToFirebase() // Send to firebase
default: break
}
}
}
struct RecipeData: Codable {
var user: String?
var recipeName: String?
var ingredientsText: String?
var directionsText: String?
var servingsNumber: String?
var prepTime: String?
var cookTime: String?
var image: String?
let id = UUID().uuidString
}
class RecipeNameCell: UITableViewCell {
#IBOutlet weak var textField: UITextField!
var id = UUID().uuidString
let db = Firestore.firestore()
var name: String?
override func awakeFromNib() {
super.awakeFromNib()
textField.placeholder = "Recipe Name..."
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
func saveData(text: String) {
if let recipeName = recipeNameTextField.text,
let addedIngredients = ingredientsTextField.text,
let directionsText = directionsTextField.text,
let servingsNum = numServingLabel.text,
let image = imageView.image,
let user = Auth.auth().currentUser?.email {
let newRecipeRef = db.collection(D.FStore.collectionName).document(id)
newRecipeRef.setData([
D.FStore.recipeTextField: recipeName,
D.FStore.ingredientsText: addedIngredients,
D.FStore.directionsText: directionsText,
D.FStore.numberServings: servingsNum,
D.FStore.userField: user,
D.FStore.id: id,
D.FStore.image: image,
]) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID:\(newRecipeRef)")
}
}
}
}
import UIKit
import Firebase
class HomeScreenViewController: UIViewController {
#IBOutlet weak var table: UITableView!
#IBOutlet weak var logout: UIBarButtonItem!
#IBOutlet weak var add: UIBarButtonItem!
let db = Firestore.firestore()
var data = [RecipeData]()
var recipeNamed: String?
override func viewDidLoad() {
super.viewDidLoad()
table.delegate = self
table.dataSource = self
navigationItem.hidesBackButton = true
table.register(UINib(nibName: "RecipeCell", bundle: nil), forCellReuseIdentifier: "cell")
}
func loadRecipeNames() {
db.collection("Recipe")
.addSnapshotListener { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let data = document.data()
if let user = data["User"] as? String,
let recipeNameLabels = data["Recipe Name"] as? String,
let prep = data["Prep Time"] as? String,
let cook = data["Cook Time"] as? String,
let servings = data["Servings"] as? String
{
let newRecipe = RecipeData(user: user, recipeName: recipeNameLabels, servingsNumber: servings, prepTime: prep, cookTime: cook)
self.data.append(newRecipe)
}
DispatchQueue.main.async {
self.table.reloadData()
}
}
}
}
}
#IBAction func logoutPress(_ sender: UIBarButtonItem) {
do {
try Auth.auth().signOut()
navigationController?.popToRootViewController(animated: true)
} catch let signOutError as NSError {
print("Error signing out: %#", signOutError)
}
}
#IBAction func addRecipePressed(_ sender: UIBarButtonItem) {
performSegue(withIdentifier: "add", sender: self)
}
}
extension HomeScreenViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = data[indexPath.row]
let cell = table.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! RecipeCell
cell.recipeNameLabel.text = data.recipeName
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "DetailSegue", sender: self)
}
}
You need to fire the sendToFirebase() function:
extension AddRecipeViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
switch textField.tag {
case 0:
let recipeName = textField.text!
print(recipeName)
objRecipe.recipeName = textField.text!
sendToFirebase() // Send to firebase
case 1:
let servingSize = textField.text!
print(servingSize)
objRecipe.servingsNumber = textField.text!
sendToFirebase()
case 2:
let prepTime = textField.text!
print(prepTime)
objRecipe.prepTime = textField.text!
sendToFirebase()
case 3:
let cookTime = textField.text!
print(cookTime)
objRecipe.cookTime = textField.text!
sendToFirebase()
default: break
}
}
}
sendToFirebase() function:
func sendToFirebase() {
let recipeName = objRecipe.recipeName
let servings = objRecipe.servingsNumber
let prep = objRecipe.prepTime
let cook = objRecipe.cookTime
let user = Auth.auth().currentUser?.email
let newRecipeRef = db.collection("Recipe").document(objRecipe.id)
newRecipeRef.setData([
"Recipe Name" : recipeName ?? "Empty",
"Serving #" : servings ?? "Empty",
"Prep Time" : prep ?? "Empty",
"Cook Time" : cook ?? "Empty",
"User" : user ?? "No email provided",
])
{ err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID:\(newRecipeRef)")
}
}
}
I'm trying do delete the note from Realm in my NotesApp and facing this error: "Can only delete an object from the Realm it belongs to". This note has been saved before also in Realm and
could display it in my TableView by tapping on the date in my FSCalendar. I tried to replace realm.add(item) with realm.create(item), but also got the error: "Cannot convert value of type 'T' to expected argument type 'Object.Type' (aka 'RealmSwiftObject.Type')". I'm new in programming, so any help would be appreciated. Here's the relevant code code:
in my ToDoListItem.swift
class ToDoListItem: Object {
#objc dynamic var noteName: String = ""
#objc dynamic var date: Date = Date()
#objc dynamic var descriptionText: String = ""
#objc dynamic var noteImage = Data()
init(date: Date, noteName: String) {
self.date = date
self.noteName = noteName
}
override init() {
self.noteName = ""
self.date = Date()
self.descriptionText = ""
self.noteImage = Data()
}
}
in my RealmManager.swift
class RealmManager {
static let shared = RealmManager()
private let realm = try! Realm()
func write<T: Object>(item: T) {
realm.beginWrite()
realm.add(item)
try! realm.commitWrite()
}
func getObjects<T: Object>(type: T.Type) -> [T] {
return realm.objects(T.self).map({ $0 })
}
func delete<T: Object>(item: T) {
try! realm.write {
realm.delete(item)
}
}
}
in my ViewController where i can edit and delete the notes
#IBAction func didTapDelete() {
let note = ToDoListItem()
RealmManager.shared.delete(item: note)
self.deletionHandler?()
navigationController?.popToRootViewController(animated: true)
}
and finally in my TableViewController where the notes are displayed (honestly i think the problem is hidden here but cannot find it...
#IBOutlet var tableViewPlanner: UITableView!
#IBOutlet var calendarView: FSCalendar!
private var data = [ToDoListItem]()
var datesOfEvents: [String] {
return self.data.map { DateFormatters.stringFromDatestamp(datestamp: Int($0.date.timeIntervalSince1970)) }
}
var items: [ToDoListItem] = []
func getCount(for Date: String) -> Int {
var count: [String : Int] = [:]
for date in datesOfEvents {
count[date] = (count[date] ?? 0) + 1
}
return count[Date] ?? 0
}
func getEventsForDate(date: Date) -> [ToDoListItem] {
let string = DateFormatters.stringFromDatestamp(datestamp: Int(date.timeIntervalSince1970))
return self.data.filter {DateFormatters.stringFromDatestamp(datestamp: Int($0.date.timeIntervalSince1970)) == string }.sorted(by: {$0.date < $1.date})
}
override func viewDidLoad() {
super.viewDidLoad()
calendarView.rounding()
tableViewPlanner.rounding()
data = RealmManager.shared.getObjects(type: ToDoListItem.self)
self.items = self.getEventsForDate(date: Date())
calendarView.delegate = self
calendarView.dataSource = self
tableViewPlanner.delegate = self
tableViewPlanner.dataSource = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
self.calendarView.select(Date())
self.calendarView.reloadData()
refresh()
}
//MARK:- TableView Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count //data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: K.plannerCellIdentifier, for: indexPath) as! NoteTableViewCell
let note = self.items[indexPath.row]
cell.configureCell(note: note)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let note = data[indexPath.row]
guard
let vc = storyboard?.instantiateViewController(identifier: K.infoVCIdentifier) as? InfoViewController else { return }
vc.note = note
vc.deletionHandler = { [weak self] in
self?.refresh()
}
vc.title = note.noteName
navigationController?.pushViewController(vc, animated: true)
}
//MARK:- User Interaction
#IBAction func didTapAddButton() {
guard
let vc = storyboard?.instantiateViewController(identifier: K.entryVCIdentifier) as? EntryViewController else { return }
vc.completionHandler = { [weak self] in
self?.refresh()
}
vc.title = K.entryVCTitle
navigationController?.pushViewController(vc, animated: true)
}
func refresh() {
DispatchQueue.main.async {
self.data = RealmManager.shared.getObjects(type: ToDoListItem.self)
self.tableViewPlanner.reloadData()
self.calendarView.reloadData()
}
}
}
extension PlannerViewController: FSCalendarDelegateAppearance & FSCalendarDataSource {
func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, eventDefaultColorsFor date: Date) -> [UIColor]? {
let dateString = DateFormatters.yearAndMonthAndDateFormatter.string(from: date)
if self.datesOfEvents.contains(dateString) {
return [UIColor.blue]
}
return [UIColor.white]
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
self.items = self.getEventsForDate(date: date)
self.tableViewPlanner.reloadData()
}
func calendar(_ calendar: FSCalendar, numberOfEventsFor date: Date) -> Int {
let dateString = DateFormatters.yearAndMonthAndDateFormatter.string(from: date)
let count = self.getCount(for: dateString)
self.tableViewPlanner.reloadData()
return count
}
}
The real problem is the didTapDelete function -- Why are you creating a new note just to delete it (I hope it was only to test out realm delete syntax). You should delete the note object that you passed to the view controller to edit / delete. (vc.note in did select row -> self.note in the other VC - where didTapDelete is)
So your did tap delete will look like --
RealmManager.shared.delete(item: note)
//show deleted alert & go back
A little explanation on the error - Just instantiating a Realm object (ToDoListItem()) does not add it to Realm (the Database system). To delete / edit a realm object, it has to be either fetched from a realm (RealmManager.shared.getObjects(type: ToDoListItem.self)) or added to the realm.
I'd advise going through a Realm tutorial before jumping in the code (there are plenty of them)
well I'm trying to add date to my header sections every time the user is done with his task. the problem is im kinda new with headers section and for some reason every time I add more than two tasks into the "Done" section, it seems like it duplicates itself.
have looked around stackOverFlow and couldn't find what I needed, and I hope u guys can help me up :)
Here's a pic of the app with the problem: https://imgur.com/wjVy9Uy
Here's my code:
import Foundation
import CoreData
import UIKit
import SwipeCellKit
protocol TaskDelegate {
func updateTaskName(name:String)
}
class TasksManViewController: UITableViewController, SwipeTableViewCellDelegate {
#IBOutlet weak var Sege: UISegmentedControl!
var tasksArray = [Task](){
didSet {
// because we perform this operation on the main thread, it is safe
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
let isSwipeRightEnabled = true
var delegate: TaskDelegate?
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
override func viewDidLoad() {
}
override func viewWillAppear(_ animated: Bool) {
loadTasks()
}
// MARK: - DataSource + Delegate Methods:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasksArray.count
}
override func numberOfSections(in tableView: UITableView) -> Int {
return tasksArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "taskCellRow") as! SwipeTableViewCell
cell.delegate = self
cell.textLabel?.text = tasksArray[indexPath.row].title
return cell
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
if orientation == .left {
guard isSwipeRightEnabled else { return nil }
let doneAction = SwipeAction(style: .destructive, title: "Done") { (action, indexPath) in
//STEP1: Append the task to the doneTasksArr:
self.tasksArray[indexPath.row].isDone = true
//STEP3: Remove the Row:
self.tasksArray.remove(at: indexPath.row)
//STEP4: Update the Model:
self.saveTasks()
self.delegate?.updateTaskName(name: "")
self.tableView.reloadData()
}
let unDoneAction = SwipeAction(style: .destructive, title: "Undone") { (unDoneAction, indexPath) in
self.tasksArray[indexPath.row].isDone = false
self.tasksArray.remove(at: indexPath.row)
self.saveTasks()
}
//configure btn:
doneAction.backgroundColor = .cyan
unDoneAction.backgroundColor = .blue
if Sege.selectedSegmentIndex == 0 { //Doing this to allow the user to unDone tasks.
return [doneAction]
} else { return [unDoneAction] }
} else {
let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in
self.context.delete(self.tasksArray[indexPath.row])
self.tasksArray.remove(at: indexPath.row)
self.saveTasks()
self.delegate?.updateTaskName(name: "")
self.tableView.reloadData()
}
return [deleteAction]
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
// MARK: - Class Methods:
#IBAction func addBtnTapped(_ sender: UIBarButtonItem) {
Sege.selectedSegmentIndex = 0 // Doing this to avoid the user to insert a task into the DoneTasks by mistake! and avoiad a bug :)
insertNewTask()
}
func insertNewTask() {
var textField = UITextField()
let alert = UIAlertController(title: "New Task", message: "Please Add Your Task", preferredStyle: .alert)
alert.addTextField { (alertTextField) in
alertTextField.placeholder = "Create New Task"
textField = alertTextField
}
let action = UIAlertAction(title: "Add", style: .default) { (action) in
let newItem = Task(context: self.context)
newItem.title = textField.text!
newItem.isDone = false
newItem.date = Date()
self.tasksArray.append(newItem)
self.delegate?.updateTaskName(name: newItem.title!)
self.saveTasks()
}
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
// MARK: - Sege Section:
#IBAction func segeControlTapped(_ sender: UISegmentedControl) {
switch Sege.selectedSegmentIndex
{
case 0:
//Loading normal tasks whose are not done
loadTasks()
case 1:
//Loading the doneTasks:
loadDoneTasksFrom()
default:
print("There's something wrong with Sege!")
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let completedTasksToDisply = 0
if Sege.selectedSegmentIndex == 1 {
if let firstTask = tasksArray[section].date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
let dateString = dateFormatter.string(from: firstTask)
return " " + dateString + " " + " " + "Completed Tasks: \(completedTasksToDisply)"
}
}
return ""
}
override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let header: UITableViewHeaderFooterView = view as! UITableViewHeaderFooterView
header.textLabel?.font = UIFont(name: "Verdana", size: 13.0)
header.textLabel?.textAlignment = NSTextAlignment.center
}
//MARK: - Model Manipulation Methods:
func saveTasks() {
do {
try! context.save()
} catch let err {
print("Error Saving context \(err)")
}
}
func loadTasks() {
let request: NSFetchRequest<Task> = Task.fetchRequest()
request.predicate = NSPredicate(format: "isDone == %#", NSNumber(value: false))
request.sortDescriptors = [NSSortDescriptor(key: "isDone", ascending: false)]
do{
tasksArray = try! context.fetch(request)
} catch {
print("There was an error with loading items \(error)")
}
tableView.reloadData()
}
func loadDoneTasksFrom() {
let request:NSFetchRequest<Task> = Task.fetchRequest()
request.predicate = NSPredicate(format: "isDone == %#", NSNumber(value: true))
request.sortDescriptors = [NSSortDescriptor(key: "isDone", ascending: false)]
do{
tasksArray = try context.fetch(request)
} catch {
print("Error fetching data from context\(error)")
}
tableView.reloadData()
}
}
EDIT: Okay I see what you are trying to do. Give this a shot - we will need a double array - each array inside will have to be split by date - our query would look something like this:
var tasksArray = [[Task]]()
var sectionDates = [String]()
func loadTasks() {
tasksArray.removeAll(keepingCapacity: false)
sectionDates.removeAll(keepingCapacity: false)
let request: NSFetchRequest<Task> = Task.fetchRequest()
request.predicate = NSPredicate(format: "isDone == %#", NSNumber(value: false))
request.sortDescriptors = [NSSortDescriptor(key: "isDone", ascending: false)]
do{
let tasks = try! context.fetch(request)
for task in tasks {
if let taskDate = task.date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
let dateString = dateFormatter.string(from: taskDate)
if self.sectionDates.contains(dateString) {
if let section = self.sectionDates.firstIndex(of: dateString) {
self.tasksArray[section].append(task)
}
} else {
self.sectionDates.append(dateString)
// EDIT - ADD THIS LINE (and self. where necessary)
self.tasksArray.append([])
if let section = self.sectionDates.firstIndex(of: dateString) {
self.tasksArray[section].append(task)
}
}
}
}
} catch {
print("There was an error with loading items \(error)")
}
tableView.reloadData()
}
Could also be helpful to load your objects in order but with this set up it should still group everything correctly regardless of the order - unless you want your sections in a specific order then you can specify in query
Now changes to your tableView delegate methods
override func numberOfSections(in tableView: UITableView) -> Int {
return sectionDates.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasksArray[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "taskCellRow") as! SwipeTableViewCell
cell.delegate = self
cell.textLabel?.text = tasksArray[indexPath.section][indexPath.row].title
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let completedTasksToDisply = 0
if Sege.selectedSegmentIndex == 1 {
let dateString = sectionDates[section]
return " " + dateString + " " + " " + "Completed Tasks: \(completedTasksToDisply)"
}
return ""
}
I figured I'd leave the rest of the tableView delegate methods to you but for the most part we just need to identify the section before the indexPath row when working with the double array - as we did in cellForRow
I ran this code and this was the result:
I've googled and read a lot of questions and answers about this topic, however I still can't solve my problem. I have a project connected to firebase, with a tableView connected to that information. The following error pops up:
unexpectedly found nil while unwrapping an Optional value.
I've checked all my connections between UIButtons, UILabels and such, and most of all i have tried using breakpoints to locate the error. However I can't seem to get my head around a solution. Here comes code and messages from the project and the error:
#IBOutlet weak var addButton: UIBarButtonItem!
let listToUsers = "ListToUsers"
var backgroundNr = 0
var items: [GroceryItem] = []
let ref = FIRDatabase.database().reference(withPath: "grocery-items")
let usersRef = FIRDatabase.database().reference(withPath: "online")
var user: User!
var userCountBarButtonItem: UIBarButtonItem!
var counter = 1
override func viewDidLoad() {
super.viewDidLoad()
if user?.email == "tor#gmail.com" {
addButton.isEnabled = false
}
tableView.allowsMultipleSelectionDuringEditing = false
userCountBarButtonItem = UIBarButtonItem(title: "1",
style: .plain,
target: self,
action: #selector(userCountButtonDidTouch))
userCountBarButtonItem.tintColor = UIColor.white
navigationItem.leftBarButtonItem = userCountBarButtonItem
usersRef.observe(.value, with: { snapshot in
if snapshot.exists() {
self.userCountBarButtonItem?.title = snapshot.childrenCount.description
} else {
self.userCountBarButtonItem?.title = "0"
}
})
ref.queryOrdered(byChild: "completed").observe(.value, with: { snapshot in
var newItems: [GroceryItem] = []
for item in snapshot.children {
let groceryItem = GroceryItem(snapshot: item as! FIRDataSnapshot)
newItems.append(groceryItem)
}
self.items = newItems
self.tableView.reloadData()
})
FIRAuth.auth()!.addStateDidChangeListener { auth, user in
guard let user = user else { return }
self.user = User(authData: user)
let currentUserRef = self.usersRef.child(self.user!.uid)
currentUserRef.setValue(self.user!.email)
currentUserRef.onDisconnectRemoveValue()
}
}
// MARK: UITableView Delegate methods
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! SubjectTableViewCell
let groceryItem = items[indexPath.row]
if backgroundNr == 0 {
cell.backgroundColor = UIColor(red: 222/255, green: 164/255, blue: 50/255, alpha: 0.6)
backgroundNr += 1
} else {
cell.backgroundColor = UIColor.white
backgroundNr -= 1
}
cell.textLabel?.text = groceryItem.name
cell.detailTextLabel?.text = groceryItem.addedByUser
toggleCellCheckbox(cell, isCompleted: groceryItem.completed)
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let groceryItem = items[indexPath.row]
groceryItem.ref?.removeValue()
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) else { return }
let groceryItem = items[indexPath.row]
let toggledCompletion = !groceryItem.completed
toggleCellCheckbox(cell, isCompleted: toggledCompletion)
groceryItem.ref?.updateChildValues([
"completed": toggledCompletion
])
}
func toggleCellCheckbox(_ cell: UITableViewCell, isCompleted: Bool) {
if !isCompleted {
cell.accessoryType = .none
cell.textLabel?.textColor = UIColor.black
cell.detailTextLabel?.textColor = UIColor.black
} else {
cell.accessoryType = .checkmark
cell.textLabel?.textColor = UIColor.gray
cell.detailTextLabel?.textColor = UIColor.gray
}
}
// MARK: Add Item
#IBAction func addButtonTapped(_ sender: Any) {
let alert = UIAlertController(title: "Kunskaps Område",
message: "Lägg till objekt",
preferredStyle: .alert)
let saveAction = UIAlertAction(title: "Spara",
style: .default) { _ in
// 1
guard let textField = alert.textFields?.first,
let text = textField.text else { return }
// 2
let groceryItem = GroceryItem(name: text,
addedByUser: self.user!.email,
completed: false)
// 3
let groceryItemRef = self.ref.child(text.lowercased())
// 4
groceryItemRef.setValue(groceryItem.toAnyObject())
}
let cancelAction = UIAlertAction(title: "Cancel",
style: .default)
alert.addTextField()
alert.addAction(saveAction)
alert.addAction(cancelAction)
present(alert, animated: true, completion: nil)
}
func userCountButtonDidTouch() {
performSegue(withIdentifier: listToUsers, sender: nil)
}
#IBAction func startClockTapp(_ sender: Any) {
Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(GroceryListTableViewController.updateCounter), userInfo: nil, repeats: true)
}
func updateCounter() {
counter += 1
let clock = String(counter)
print(clock)
let clockCurrent = Clock(name: clock, addedByUser: self.user!.email)
let clockCurrentRef = self.ref.child(clock.lowercased())
clockCurrentRef.setValue(clockCurrent.toAnyObject())
}
When using breakpoints to locate where the error occurs, it seems that it is located in the following function: (the first line of code in "UITable view delegate methods)
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
I don't now if I'm using the breakpoints wrong, however that's the result I get, if it helps.
The "grocery" objects struct file´s code is the following:
import Foundation
import Firebase
struct GroceryItem {
let key: String
let name: String
let addedByUser: String
let ref: FIRDatabaseReference?
var completed: Bool
init(name: String, addedByUser: String, completed: Bool, key: String = "") {
self.key = key
self.name = name
self.addedByUser = addedByUser
self.completed = completed
self.ref = nil
}
init(snapshot: FIRDataSnapshot) {
key = snapshot.key
let snapshotValue = snapshot.value as! [String: AnyObject]
name = snapshotValue["name"] as! String
addedByUser = snapshotValue["addedByUser"] as! String
completed = snapshotValue["completed"] as! Bool
ref = snapshot.ref
}
func toAnyObject() -> Any {
return [
"name": name,
"addedByUser": addedByUser,
"completed": completed
]
}
}
The database content on Firebase gets structured as following:
educationlevel-e230e
grocery-items
Fysik:, addedByUser:, completed:
The full string of the basic error message:
fatal error: unexpectedly found nil while unwrapping an Optional value
2016-12-24 01:22:54.799117 EducationLevel[1077:263810]
fatal error:
unexpectedly found nil while unwrapping an Optional value
I really can't find the answer to this problem by my self or anywhere on the web, REALLY appreciates it if I could get some help (sorry for the probably bad english, I'm from Sweden;) )
(If you need more strings from the error message or anything like that I'll immediately update this question)
Thanks!! Tor from Sweden.
I have an Instagram like feed with pictures shown one at a time in each cell. You can press on them and get directed to a commentTBV. Here is where I have all the comments underneath the post and they get counted correctly in this View.
var commentsArray = [FotoComment]()
is the array holding the comments
kommentarArray is the array i want to fill with the assignArray func so i can use it to display the amount of counts.
var kommentarArray = [FotoComment]()
[FotoComment] is my struc I use for my comments
What I want is that already in the feed the commentArray.count will show the correct number of comments.
func assignArray() {
let otherVC = CommentTableViewController()
kommentarArray = otherVC.commentsArray
print(kommentarArray.count)
}
This way I get access from my feed to the array of comments in the CommentTBVC.
My cell is:
cell.kommentarZähler.text = "Kommentare: \(kommentarArray.count)"
But it always shows 0 even though it already has 5 comments and it correctly displayed on the CommentTBV.
M complete code for MemesTableViewConbtroller (the feed)
import UIKit
import Firebase
import FirebaseAuth
import FirebaseDatabase
class MemesTableViewController: UITableViewController {
var kommentarArray = [FotoComment]()
var dataBaseRef : FIRDatabaseReference!
var storageRef : FIRStorageReference!
var posts = [PostMitBild]()
var segmentedControl : HMSegmentedControl!
override func viewDidLoad() {
super.viewDidLoad()
assignArray()
segmentedControl = HMSegmentedControl(sectionTitles: ["Top Heute", "Beliebteste", "Neue"])
segmentedControl.frame = CGRect(x: 10, y: 10, width: 300, height: 60)
segmentedControl.backgroundColor = UIColor.red
segmentedControl.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.white]
segmentedControl.borderColor = UIColor.brown
segmentedControl.tintColor = UIColor.black
segmentedControl.selectionIndicatorColor = UIColor.gray
segmentedControl.addTarget(self, action: #selector(getter: MemesTableViewController.segmentedControl), for: UIControlEvents.valueChanged)
tableView.tableHeaderView = segmentedControl
segmentedAction()
}
func segmentedAction() {
if segmentedControl.selectedSegmentIndex == 0 {
let postRef = FIRDatabase.database().reference().child("MemesBilder")
postRef.observe(.value, with: { (snapshot) in
var newPost = [PostMitBild]()
for post in snapshot.children {
let Post = PostMitBild(snapshot: post as! FIRDataSnapshot)
newPost.insert(Post, at: 0)
}
self.posts = newPost
DispatchQueue.main.async {
self.tableView.reloadData()
}
}, withCancel: { (error) in
print(error.localizedDescription)
})
}
}
//------------------------------------------
override func viewWillAppear(_ animated: Bool) {
if FIRAuth.auth()?.currentUser == nil {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Login")
self.present(vc, animated: true, completion: nil)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell
if let seconds = posts[indexPath.row].postDate {
let timestamp = NSDate(timeIntervalSince1970: seconds)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy HH:mm"
cell.uploadDatum.text = dateFormatter.string(from: timestamp as Date)
}
cell.kommentarZähler.text = "Kommentare: \(kommentarArray.count)"
cell.usernameTextField.text = posts[indexPath.row].username
cell.postContent.text = posts[indexPath.row].content
storageRef = FIRStorage.storage().reference(forURL: posts[indexPath.row].userImageUrlString)
storageRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
if error == nil {
DispatchQueue.main.async {
if let data = data {
cell.UserImageView.image = UIImage (data: data)
}
}
}else {
print(error?.localizedDescription)
}
})
let storageRef2 = FIRStorage.storage().reference(forURL: posts[indexPath.row].PostImageUrlString)
storageRef2.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
if error == nil {
DispatchQueue.main.async {
if let data = data {
cell.postImageView.image = UIImage (data: data)
}
}
}else {
print(error?.localizedDescription)
}
})
return cell
}
//done!!!! ------------------------------------------
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
tableView.deleteRows(at: [indexPath], with: .fade)
let ref = posts[indexPath.row].ref
ref!.removeValue()
posts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var numberOfRows = 0
switch segmentedControl.selectedSegmentIndex {
case 0 : numberOfRows = posts.count
case 1: numberOfRows = posts.count
default: break
}
return numberOfRows
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 420.00
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segmentedControl.selectedSegmentIndex == 0 {
if segue.identifier == "addComment" {
let vc = segue.destination as! CommentTableViewController
let indexPath = tableView.indexPathForSelectedRow!
vc.selectedPosts = posts[indexPath.row]
}
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if segmentedControl.selectedSegmentIndex == 0 {
performSegue(withIdentifier: "addComment", sender: self)
}
if segmentedControl.selectedSegmentIndex == 1 {
performSegue(withIdentifier: "addComment", sender: self)
}
if segmentedControl.selectedSegmentIndex == 2 {
performSegue(withIdentifier: "addComment", sender: self)
}
}
func assignArray() {
let otherVC = CommentTableViewController()
kommentarArray = otherVC.commentsArray
print(kommentarArray.count)
}
}
The code for the CommentTableViewController ( where i want to get the count of comments from the array var commentsArray = FotoComment which is already working on this TableView)
import UIKit
import Firebase
import FirebaseAuth
import FirebaseDatabase
import FirebaseStorage
class CommentTableViewController: UITableViewController {
#IBOutlet weak var komentarZähler: UILabel!
#IBOutlet weak var UserImageView: UIImageView!
#IBOutlet weak var usernameTextField: UILabel!
#IBOutlet weak var postImageView: UIImageView!
#IBOutlet weak var postContent: UITextView!
var dataBaseRef : FIRDatabaseReference!
var storageRef : FIRStorageReference!
var commentsArray = [FotoComment]()
var selectedPosts:PostMitBild!
override func viewDidLoad() {
super.viewDidLoad()
configurePost()
let commentRef = selectedPosts.ref!.child("Kommentare")
commentRef.observe(.value, with: { (snapshot) in
var newComments = [FotoComment]()
for item in snapshot.children {
let neuerKommentar = FotoComment(snapshot: item as! FIRDataSnapshot)
newComments.insert(neuerKommentar, at: 0)
}
self.commentsArray = newComments
self.tableView.reloadData()
}, withCancel: { (error) in
print(error.localizedDescription)
})
}
#IBAction func addComment(_ sender: UIBarButtonItem) {
let alertView = UIAlertController(title: "Kommentar", message: "Füge einen Kommentar hinzu", preferredStyle: UIAlertControllerStyle.alert)
alertView.addTextField { (textfield) in
textfield.placeholder = "Einen neuen Kommentar hinzufügen"
}
let sendCommentAction = UIAlertAction(title: "Kommentieren", style: .default) { (action) in
let textfield = alertView.textFields!.first!
let comment = FotoComment(content: textfield.text! , postId: self.selectedPosts.postId , username: (FIRAuth.auth()!.currentUser!.displayName!) , userImageUrlString: String(describing: FIRAuth.auth()!.currentUser!.photoURL!), postDate: (NSDate().timeIntervalSince1970))
let commentRef = self.selectedPosts.ref!.child("Kommentare").childByAutoId()
commentRef.setValue(comment.toAnyObject())
}
let cancelAction = UIAlertAction(title: "Abbrechen", style: .cancel, handler: nil)
alertView.addAction(sendCommentAction)
alertView.addAction(cancelAction)
self.present(alertView, animated: true, completion: nil)
}
// 2----------------------------------------------
func configurePost() {
usernameTextField.text = selectedPosts.username
postContent.text = selectedPosts.content
storageRef = FIRStorage.storage().reference(forURL: selectedPosts.userImageUrlString)
storageRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
if error == nil {
DispatchQueue.main.async {
if let data = data {
self.UserImageView.image = UIImage (data: data)
}
}
}else {
print(error?.localizedDescription)
}
})
let storageRef2 = FIRStorage.storage().reference(forURL: selectedPosts.PostImageUrlString)
storageRef2.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
if error == nil {
DispatchQueue.main.async {
if let data = data {
self.postImageView.image = UIImage (data: data)
}
}
}else {
print(error?.localizedDescription)
}
})
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
komentarZähler.text = "Kommentare: \(commentsArray.count)"
return commentsArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "commentCell", for: indexPath) as! CommentTableViewCell
if let seconds = commentsArray[indexPath.row].postDate {
let timestamp = NSDate(timeIntervalSince1970: seconds)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy HH:mm:ss"
cell.uploadDatum.text = dateFormatter.string(from: timestamp as Date)
}
cell.usernameTextField.text = commentsArray[indexPath.row].username
cell.postContent.text = commentsArray[indexPath.row].content
storageRef = FIRStorage.storage().reference(forURL: commentsArray[indexPath.row].userImageUrlString!)
storageRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
if error == nil {
DispatchQueue.main.async {
if let data = data {
cell.UserImageView.image = UIImage (data: data)
}
self.tableView.reloadData()
}
}else {
print(error?.localizedDescription)
}
})
return cell
}
}
this is how i safe my data in firebase. I want to update the number of comments (kommentarAnzahl which is currently 0) every time a comment is added to the post
You have to convert your Array count to string value to display in cell.
Try this Code,
cell.kommentarZähler.text = String("Kommentare:",kommentarArray.count)
I hope this will work for You.
So you have two separate view controllers namely CommentTableViewController and MemesTableViewController consisting of commentsArray and kommentarArray resepctively. These arrays are of class-scope meaning that once you get outside of your class; for this case it would be leaving the view, then they would be deallocated. Once you enter the view, they'll be re-created again and filled with values.
Since you want to get the number of elements in commentsArray, I'd recommend you create a static variable which would keep track of this. When you make something static you're pretty much making it accessible throughout your entire app/program and changes made to it are reflected across the entire app. In other words, a memory block is reserved to store your variable which will only be de-allocated once you quit the app entirely. You may think of this as an app-scope variable.
Two ways
Lousy Way
Change your commentsArray definition from var commentsArray = [FotoComment]() to static var commentsArray = [FotoComment](). By doing this, then you may access and manipulate the contents of this array from any other class. This is great if you have few elements but what happens when you have tens of thousands or even a million comments? It'll mean that we'll be walking around with huge amounts of data everywhere even when we really don't need it.
Recommended Way
Keep your current commentsArray definition and add this static var numberOfComments: Int = 0 inside your CommentTableViewController. Right after adding your elements to commentsArray, update the element tracker as shown below
CommentTableViewController.numberOfComments = commentsArray.count
Then when you go back to your MemesTableViewController, you can simply delete assignArray(); since we have a global element counter now, and simply amend your cell to this
cell.kommentarZähler.text = String(CommentTableViewController.numberOfComments)
With this, even if you create say another class called FriendsVC, you can still access numberOfComments and even change it as well.
PS: Since numberOfComments is static, whenever and wherever you want to access or amend it, you MUST always first call the class or struct in which it was defined in. For this case, it is inside CommentTableViewController so you need to always do CommentTableViewController.numberOfComments to access it; even when you are inside CommentTableViewController itself. Always remember that.