I'm having a issue with xcode, since I updated to 8.3.3. I usually work with the MCV (Model - View - Controller) method, and now, my variables aren't updating between them.
Situation: I Have a Model (store all major variables and calculations functions); a TableView Controller (Control Tableview) and TableViewCell (set Outlet and actions)
Goal: When a button is pressed in a cell, it should add a row in TableView.
Problem: Why isn't table view getting the new value of Model() variable.
To make it better to understand, here is a timeline of what is going on :
Run > run viewDidLoad in TableViewController > update variable test in Model() > cellForRowAt is called and prints ["1"] > show tableView with 1 row > press button > print ["1"] > add ["2"] to Model() > print ["1","2"] > post notification > viewDidLoad gets Notification and prints "reloading table" > cellForRowAt is called and prints ["1"] > tableView keeps 1 row.
Here is one example of my code:
I have my Model.Swift:
class Model {
var test : [String] = []
}
My TableViewController:
class BudgetTableViewController: UITableViewController {
let model = Model()
override func viewDidLoad() {
super.viewDidLoad()
model.test.append("1")
center.addObserver(forName: NSNotification.Name(rawValue: "reloadTableVIew"), object: appDelegate, queue: queue) {[unowned self] (_) in
print("reloading table")
self.tableView.reloadData()
}
}
To simplify.. in Sections I keep returning "1" and for rows I count the variable test in Model().
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print(model.test) // **ALWAYS PRINT ["1"]**
let cell = tableView.dequeueReusableCell(withIdentifier: "buttonCell", for: indexPath) as! TableViewCell
return Cell
}
And my TableViewCell:
class TableViewCell: UITableViewCell{
let model = BudgetModel()
let notification = Notification(name: Notification.Name(rawValue: "reloadTableVIew"), object: appDelegate)
#IBAction func okButton(_ sender: UIButton) {
print(model.test)
model.test.append("2")
print(model.test)
NotificationCenter.default.post(notification)
}
}
I hope it is clear enough.
The point is, if I press the button 5 times, it will add the string 5 times to the array (that is confirmed in the print) but when cellForRowAt is called, it will print ["1"] always.
THank you for your help
The two model vars are in different classes. You have var model in your viewController, & var model in your cell. Changing one isn't going to affect the other. You'd be best to implement a delegate for your cells, where the viewController is the delegate, and the cell calls it when pressed -
protocol MyCellDelegate: class {
func cellWasPressed()
}
In the cell -
weak var delegate: MyCellDelegate?
Set this to the viewController when you create the cell.
Then in the button pressed method, add -
self.delegate?.cellWasPressed()
In the viewController, implement this -
func cellWasPressed() {
self.model.test.append("2")
self.tableView.reloadData()
}
I don't think you need to be using a notification, this is far simpler.
Related
That is my footerView called FooterTableViewCell. I have this protocol called SurveyAnswerTableViewCellDelegate. It's parent is AddQuestionViewController.
When I tap on the footerView I trigger #IBActtion.
#objc protocol SurveyAnswerTableViewCellDelegate: AnyObject {
func textSaved(_ text: String)
}
class FooterTableViewCell: UITableViewHeaderFooterView {
var parentVC: AddQuestionViewController!
#IBAction func addNewTapped(_ sender: Any) {
print("tapped")
let newTag = model.tag + 1
parentVC.addNewAnswer()
}
This button action triggers AddQuestionViewController
class AddQuestionViewController: SurveyAnswerViewDelegate, UITextFieldDelegate, UITableViewDelegate, SurveyAnswerTableViewCellDelegate {
var answers: [SurveyAnswerModel] = []
var savedText : String = ""
static var delegate: SurveyAnswerTableViewCellDelegate?
I try creating an empty string and append a new answer to my array. But this text here is always "".
func addNewAnswer() {
let newAnswer = SurveyAnswerModel(answer: savedText, tag: 0)
self.answers.append(newAnswer)
self.tableView.reloadData()
}
func textSaved(_ text: String) {
savedText = text
}
The textfield I try to read is inside SurveyAnswerTableViewCell while setting up the cell inside the tableview I call setup function.
class SurveyAnswerTableViewCell: UITableViewCell {
#IBOutlet weak var textField: UITextField!
weak var delegate: SurveyAnswerTableViewCellDelegate?
var parentVC: AddQuestionViewController!
func setup() {
if let text = self.textField.text {
self.delegate?.textSaved(textField.text!)
}
}
extension AddQuestionViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as SurveyAnswerTableViewCell
cell.parentVC = self
cell.setup()
return cell
}
How can I successfully send that text to AddQuestionViewController so it appends a new answer with correct string
There are a few things keeping this from working.
You are calling SurveyAnswerTableViewCell's setup() function directly after dequeuing the cell for reuse. It has not yet (re)appeared on the screen at that point, so the user has not had a chance to enter anything into the text field.
You don't currently set the delegate property of SurveyAnswerTableViewCell to anything, so even if the textfield had valid input, the delegate would be nil and delegate?.textSaved(textField.text!) wouldn't do anything.
Both of the previous points mean that the value of AddQuestionViewController .savedText never gets updated from the empty string. So when addNewAnswer() tries to read it, it will always see that empty string.
Rather than reading the text field when the cell is dequeued, it would make more sense to save the text field value when the user is done typing.
To do that, conform the cell to UITextFieldDelegate and implement the textFieldDidEndEditing(_:) method. From within that method you can then call the delegate method you already have to save the text. Make sure the delegate property on the cell has been set by the VC, or else this won't do anything!
The VC itself should not have a delegate property of type SurveyAnswerTableViewCellDelegate. It serves as the delegate, rather than having one. If this doesn't quite make sense, I would recommend reviewing some online resources on the delegate pattern.
So make sure the ViewController conforms to SurveyAnswerTableViewCellDelegate and then set the cell's delegate value to the VC. The cellForRowAt function should then look something like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as SurveyAnswerTableViewCell
cell.delegate = self
return cell
}
As a side note, neither the footer nor the cell should have a reference to the parent view controller. as a general rule it is good to avoid subviews being aware of their parent views. Things get unnecessarily complicated when there is two-way knowledge sharing between components, and it makes the subview much less reusable. I would recommend making a delegate for the footer as well, and removing the parentVC property from both the footer and the cell.
Here's what it looks like is happening:
Button tapped
addNewTapped(_:) invoked
addNewAnswer() invoked
newAnswer is appended to answers
tableView.reloadData() invoked
Cells are regenerated with new/empty textfields (so delegate.textSaved is never invoked)
so I'm not sure what you're trying to do, but here's what I figure are a couple possible routes:
store UITextFields separately and add them into table cells so they're not removed by a table reload
conform AddQuestionViewController to UITextFieldDelegate and set it as the textfields' delegate to observe textfield texts changing (and if you're only using 1 textfield, you could set savedText there)
I have a UIView which displays some information such as a user's Name and more, including a list of objects that all get pulled from my database. This works fine.
However, I now have a ViewController that gets presented on top of the current ViewController. In this presented ViewController, I am adding Data to my Database. When dismissing that view, I want the original ViewController to update all of its content to be up to date.
Right now, all my views are getting layedout in ViewDidLoad, meaning that they only really get loaded once and don't reload later on. I have managed to update Layout by calling self.view.layoutIfNeeded(), but if I understand correctly, this only updates constraint. Of course, I could call a new init of my original view controller. This would make it reload, but I would like to avoid that.
Another Idea I had was to set up all my content in the ViewWillAppear, which should maybe then update anytime my view controller is about to be visible. However, I don't know how to go about doing this. Can I just move all my setup code to viewWillAppear? Does this have any disadvantages?
TLDR: Is there a way to update a stackview with new elements without having to reload the full ViewController over ViewWillAppear?
The UITableView element works very smoothly with database data. If you fetch the data from your database inside viewDidLoad in your first view controller, and store it in an array, the UITableView (if you set up its dataSource correctly) will automatically populate the table with the new values from the second view controller. With this method, there is no need to use ViewWillAppear at all.
It sounds like as of now, you're using Views (inside a VStack)? to display individual objects from the database. If you want to keep whatever custom style/layout you're using with your views, this can be done by defining a custom subclass of UITableViewCell and selecting the "Also create XIB file" option. The XIB file lets you customize how the cells in your UITableView look.
Here is a simple example to show the database values in the first view controller automatically updating. I didn't include the custom XIB file (these are all default UITableViewCells), to keep it streamlined.
FIRST VIEW CONTROLLER
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var dataTable: UITableView!
var tableRows: [DataItem] = []
func loadData() {
let request: NSFetchRequest<DataItem> = DataItem.fetchRequest()
do {
tableRows = try Global_Context.fetch(request)
} catch {
print("Error loading data: \(error)")
}
}
override func viewDidLoad() {
super.viewDidLoad()
dataTable.dataSource = self
loadData()
}
#IBAction func goForward(_ sender: UIButton) {
self.performSegue(withIdentifier: "toSecond", sender: self)
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableRows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "dataTableCell", for: indexPath)
cell.textLabel?.text = tableRows[indexPath.row].name
return cell
}
}
let Global_Context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func saveContext () {
if Global_Context.hasChanges {
do {
try Global_Context.save()
} catch {
let nserror = error as NSError
print("Error saving database context: \(nserror), \(nserror.userInfo)")
}
}
}
SECOND VIEW CONTROLLER:
import UIKit
import CoreData
class AddViewController: UIViewController {
#IBOutlet weak var itemEntry: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
itemEntry.delegate = self
}
#IBAction func addNewItem(_ sender: UIButton) {
let newDataItem = DataItem(context: Global_Context)
newDataItem.name = itemEntry.text
saveContext()
}
#IBAction func goBack(_ sender: UIButton) {
self.performSegue(withIdentifier: "toFirst", sender: self)
}
}
extension AddViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.endEditing(true)
return true
}
}
Main.storyboard:
Once you set up your view controller as a UITableViewDataSource (as in the example code), the table view should make things simpler by eliminating any need to manually manage individual Views.
Is this the functionality you were looking for? (Note about the example: it was set up in Xcode with "Use Core Data" enabled.)
Here is a link to the official documentation:
https://developer.apple.com/documentation/uikit/uitableview
I am passing data from one tableview to another. I want the category data that the tableviewA contains to be passed to tableviewB. When I perform the segue, the print data that I have for TableviewB is empty.
This is tableviewA
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let category = listOfCategories[indexPath.row].strCategory
let vc = MealsByCategoryVC()
vc.mealCategory = category
print(category) // Properly returns the category
performSegue(withIdentifier: "searchMeals", sender: nil)
}
This is tableview2
class MealsByCategoryVC: UITableViewController {
var mealCategory : String = ""
var listOfMeals : [Meals] = []
override func viewDidLoad() {
super.viewDidLoad()
print("Meal category is \(mealCategory)") //This statement returns "Meal category is "
}
This:
let vc = MealsByCategoryVC()
vc.mealCategory = category
performSegue(withIdentifier: "searchMeals", sender: nil)
...is not how you pass a value into a view controller that you are creating by calling performSegue. The first two lines of that code do nothing at all! The view controller created by the segue is different from the one you are creating by saying MealsByCategoryVC(); in fact, the latter is just thrown away, uselessly. You are setting the mealCategory of the wrong view controller instance.
Instead, implement prepare(for:sender:). That's what it's for. You receive the segue and its destination view controller. That is the view controller whose mealCategory you need to set.
I have been searching through many posts on SO but couldn't find an answer to this one.
I have a Table view listing various items. Each cell in the table view has a button that swaps an image around when clicked on, effectively working as an "on" or "off" button to show a user which items in the list they have selected. I have a variable inside my custom Cell Prototype class which stores a value of true or false which is updated every time the button is clicked on.
There is a "Done" button in the Table View that when tapped on calls an unwind Segue to go back to the first View Controller.
When the user taps on the Enter Table View button (on the first View Controller) to display the Table the buttons all go back to their default state, am guessing because each time the segue to the Table View Screen happens it creates a new instance of the Table to be displayed.
What I'm trying to achieve is that the state of the button (either on or off) is retained when going back into the Table screen. I've tried for a while sending an Integer value back from the TableCell Class (using a delegate) to the first View controller and then passing that value back into the Table View controller when the forward segue is called in order to have a "retained from the previous state value" that can be compared against when the cells are created to indicate if a button had been clicked or not. Couldn't get it to work though to save the state of the buttons. The list of Items in the Table will also change depending on what a user adds.
Some Screen shots and the code are below. The code hasn't got the delegate i was trying included (as it didn't work) but if its needed I will edit the post to include it. Any help would be hugely appreciated. Many Thanks!!
Code I have so far is:
//Main View Controller//
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
//segue to the Table View Screen
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "EnterTableView" {
let newTableView = segue.destination as! TableViewController
}
}
// Unwind Segue Called on Exit From Table View
#IBAction func unwindToMainViewController (_segue:UIStoryboardSegue) {
}
}
// Table View Controller //
class TableViewController: UITableViewController {
var dataArray = ["A", "B", "C", "D"]
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
//create one section for table
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//create number of rows based on the number of items in the dataArray set above
return dataArray.count
}
// an array that will contain all the cells
var cellArray = [UITableViewCell] ()
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//create a new cell based on the cell class "TableViewCell"
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell
// variable to contain a single element from the dataArray - updates every time a new cell is created
let dataArrayForCells = dataArray [indexPath.row]
//set cell label text to show the value from the dataArrayForCells
cell!.label.text = dataArrayForCells
cellArray.append(cell!)
return cell!
}
// Table Cell Class//
class TableViewCell: UITableViewCell {
#IBOutlet weak var label: UILabel! // label to hold display some text
#IBOutlet weak var buttonImage: UIImageView! //image for button
var buttonClicked = true //variable to contain weather the has been clicked
//if button is tapped on run the below
#IBAction func aButton(_ sender: Any) {
//if button is clicked is true, swap image to red ("on") button, set buttonClicked value to false
if buttonClicked {
buttonImage.image = #imageLiteral(resourceName: "Rec Button Red")
buttonClicked = false
}
// if buttonClicked value is false swap image to grey ("off") button set buttonClicked value back to true
else {
buttonImage.image = #imageLiteral(resourceName: "Rec Button Grey")
buttonClicked = true
}
}
I have a UITextView inside each cell of a UITableView
I am using Core Data to save data which is typed in the UITextView
I would like to save the text typed in UITextView once the user is done editing it
I have added UITextViewDelegate to my TableViewCell class
I am using Notifications to post the new text to the TableViewController
I am able to get the new text to the TableViewController but I don't know how to get the row number of the cell that contained the textview wherein the text was typed. I need to know the row number (or the object in that row) to update the correct NSManagedObject.
What I have Tried:
I was thinking about using tags but since I need to constantly add and delete rows it wouldn't be the best approach
I have tried using DidSelectedRowAtIndexPath but it doesn't get triggered while the
user taps the UITextView (UITextView covers up to 80% of the one cell)
In the TableViewCell class, I have:
func textViewDidEndEditing(_ textView: UITextView) {
// Post notification
let userInfo = [ "text" : textView.text]
NotificationCenter.default.post(
name: UITextView.textDidEndEditingNotification,
object: nil,
userInfo: userInfo as [AnyHashable : Any])
}
In the TableViewController, I have:
//Subscribe
NotificationCenter.default.addObserver(self,
selector: #selector(textEditingEnded(notification:)),
name: UITextView.textDidEndEditingNotification,
object: nil)
#objc func textEditingEnded(notification:Notification) {
guard let text = notification.userInfo?["text"] as? String else {return}
print ("text: \(text)")
}
Don't hesitate to ask for more details.
I'll appreciate every bit of help I can get!
Create a property of the NSManagedObject type in the table view cell.
In Interface Builder connect the delegate of the text view to the cell.
In the controller pass the appropriate data source item in cellForRowAt to the cell.
Delete the observer and instead of posting a notification change the attribute in the NSManagedObject instance directly and save the context.
As NSManagedObject instances are reference types the changes will persist.
I hope you can have variable inside your UITableViewCell subclass for certain item
var item: Item?
then in cellForRowAt set certain item for certain cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ...
...
cell.item = array[indexPath.row]
...
}
now you can implement UITextViewDelegate to your cell subclass and you can use method textViewDidEndEditing for handling when user is done with typing
class YourCell: UITableViewCell {
...
var item: Item?
...
override func awakeFromNib() {
yourTextView.delegate = self
}
}
extension YourCell: UITextViewDelegate {
func textViewDidEndEditing(_ textView: UITextView) {
... // here save text and here you can use variable `item`
}
}