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
Related
I'm new to coding, so please bear with me. I was following an online tutorial that worked with plists to make a habit list app. I have a table view controller that shows a list of habits and a segue that presents modally a view controller that has text fields to add a habit.
enter image description here
Every time it runs, nothing happens when I click on the "save" and "cancel" buttons. I realize this is a vague question as it doesn't pinpoint to a specific issue, but I am really struggling with fixing this issue and would really appreciate if someone proofreads the code. The app builds and runs with no warnings.
This is the table view controller that shows the habits:
class TableViewController: UITableViewController {
//MARK: Properties
var habits = [Habit]()
//MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return habits.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") else {
fatalError("The dequeued cell is not an instance of ViewController.")
}
// Fetches the appropriate habit for the data source layout.
let habit = habits[indexPath.row]
cell.textLabel?.text = habit.mainGoal
cell.detailTextLabel?.text = habit.microGoal
return cell
}
#IBAction func unwindToHabitList(sender: UIStoryboardSegue) {
if let source = sender.source as?ViewController, let habit = source.habit {
//add a new habit
let newIndexPath = IndexPath(row: habits.count, section: 0)
habits.append(habit)
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
This is the view controller that adds a habit:
class ViewController: UIViewController, UITextFieldDelegate, UINavigationControllerDelegate {
#IBOutlet weak var saveButton: UIBarButtonItem!
#IBOutlet weak var mainGoalTextField: UITextField!
#IBOutlet weak var microGoalTextField: UITextField!
var habit: Habit?
//method for configuring controller before presenting
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
//configure this destination view controller only when save button is pressed
guard let button = sender as? UIBarButtonItem, button === saveButton else {
os_log("save button was not pressed, cancelling", log: OSLog.default, type: .debug)
return
}
let mainGoal = mainGoalTextField.text ?? ""
let microGoal = microGoalTextField.text ?? ""
//set the habit to be passed on to tableViewController after the unwind segue
habit = Habit(mainGoal: mainGoal, microGoal: microGoal)
}
#IBAction func cancel(_ sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
let isPresentingInAddHabitMode = presentingViewController is UINavigationController
if isPresentingInAddHabitMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
else {
fatalError("The ViewController is not inside a navigation controller.")
}
}
I appreciate any and all help in advance!
STORYBOARD CONNECTIONS:
TABLEVIEW CONTROLLER CONNECTIONS
ADD HABIT VIEW CONTROLLER CONNECTIONS
I've spent the whole day trying to figure this out, would like some insight into what's happening here:
I have a custom UITableViewController that's embedded in a navigation controller, and it has a button that triggers a segue to a plain UIViewController.
The plain UIViewController has a button that unwinds to the TableViewController, as well as a "back" button that's given for free thanks to navigation controller.
Now the issue: When I segue from TableViewController to UIViewController, everything works. From UIViewController, if I press the "back" button, it winds back and the TableView is reloaded automatically. HOWEVER, when I press the button that is linked to #IBAction to unwind, it unwinds to the TableViewController but nothing shows up, not even an empty table.
My debug view showed that once I tried to unwind using my button (not "back), TableViewController came back on screen but its frame was super small and nothing was in it, so the black background of the navigation controller was showing, thus just a black screen where the table should be.
Can anyone tell me how to manually reinitialize the tableView or reset the TableViewController when I unwind? I tried tableView.reload() in viewDidAppear() and nothing happened, as I suspect the problem is the TableViewController itself not having the original frame as before segueing.
Link to screenshots with descriptions
TableViewController code:
class StatsTableViewController: UITableViewController {
//variable stuff
#IBAction func unwindToRoot(_ sender: UIStoryboardSegue) {
}
override func viewDidLoad() {
super.viewDidLoad()
self.getData()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.async {
self.tableView.reloadData()
}
self.getData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return solves.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! StatsTableViewCell
cell.timeLabel?.text = solves[indexPath.row].0
cell.scrambleLabel?.text = solves[indexPath.row].1
return cell
}
//other stuff
}
Set Delegate Datasource Methods
//Add this two lines in viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
self.getData()
//Add this line
self.UITableViewName.delegate = self
self.UITableViewName.dataSource = self
}
You are using this two methods in statusTableViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "SelectionSegue" {
//Your code
}
}
#IBAction func unwindToThisView(sender: UIStoryboardSegue) {
// set Delegate datasource method
}
set outlet for unwindToThisView button to see the images
1.Click exit button for last ViewController
see Presenting Segues outlet for the Back Button
set action for back button
My NSCollectionView crashes when calling makeItem(withIdentifier identifier: String, for indexPath: IndexPath). numberOfItemsInSection returns the correct value. If I call makeItem... in viewDidLoad rather than in itemForRepresentedObject I see an error indicating that the indexPath is out of bounds. How can this be?
The collection view loads like this:
class TagCollectionViewController: NSViewController, NSCollectionViewDataSource {
fileprivate static let itemIdentifier = "TagItem"
#IBOutlet var collectionView: NSCollectionView!
fileprivate var tags = List<Tag>.init()
override func viewDidLoad() {
super.viewDidLoad()
let nib = NSNib(nibNamed: "TagCollectionViewItem", bundle: nil)
collectionView.register(nib, forItemWithIdentifier: TagCollectionViewController.itemIdentifier)
collectionView.dataSource = self
}
(The List collection is a Realm class)
During viewWillAppear() the tags collection is populated from a ReSwift state:
override func viewWillAppear() {
for image in mainStore.state.selectedImages {
for tag in image.tags {
tags.append(tag)
}
}
super.viewWillAppear()
}
Solved it.
When I created the .xib for the item I added an NSCollectionViewItem object but didn't wire up the view to my custom view.
To recap, for anyone who gets caught out by this, the steps to creating a NSCollectionViewItem are:
Create the nib and configure your views
Add an NSCollectionViewItem object to your nib
Wire up your view (and any other views) to the object
Register the nib with the collectionView in your view controller
I have a tableview in swift. Each cell has a label with different text. When a cell is selected it passes the text to a label on another view controller. I can save this data using a button (Save Data) and load the saved data with another button (Load Data).
My problem is I would ideally like to not use buttons, but have the data loaded automatically when the view loads. However, when I place the NSUSerDefaults code within viewDidLoad (), it saves the data but I am no longer able to change the selection i.e. it loads the first selection permanently. I have posted my code below for the destination ViewController and for the TableViewController.
ViewController
import UIKit
class DetailViewController: UIViewController {
#IBOutlet weak var detailLabel: UILabel!
var passedValue: String?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
detailLabel.text = passedValue
}
#IBAction func saveDataClicked(sender: AnyObject) {
var defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(self.detailLabel.text, forKey: "optionValue")
}
#IBAction func loadDataClicked(sender: AnyObject) {
var defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
if let optionValueIsNotNill = defaults.objectForKey("optionValue") as? String {
self.detailLabel.text = defaults.objectForKey("optionValue") as! String
}
}
}
TableViewController
import UIKit
class TableViewController: UITableViewController {
var valueToPass:String!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
// Get Cell Label
let indexPath = tableView.indexPathForSelectedRow;
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as UITableViewCell!;
valueToPass = currentCell.textLabel!.text
performSegueWithIdentifier("showDetailView", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?){
if segue.identifier == "showDetailView" {
// initialize new view controller and cast it as your view controller
var viewController = segue.destinationViewController as! DetailViewController
// your new view controller should have property that will store passed value
viewController.passedValue = valueToPass
}
}
}
if You want to save selected Data to NSUSerDefaults You should write your saving code in didSelectRowAtIndexPath like this
if you have a function to save the data in NSUserDefaults
save(DataArray[indexPath.row])
then it will be saved , but i think if your data used when the application is running only , i think the best option to use is static instance .
I'm studying programming in Swift, and I've got this example from a book with a mistake (commented). How can I fix this and update tableView while pressing the Add button? I've read many solutions, but none seem to work. Also, tableView updated when I reloaded simulator. I've been stuck with this for two days, and it's driving me mad! :)
import UIKit
import CoreData
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var managedObjectContext: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
managedObjectContext = appDelegate.managedObjectContext! as NSManagedObjectContext
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func loadBooks() -> Array<AnyObject> {
var error: NSError? = nil
var fetchRequest = NSFetchRequest(entityName: "Book")
let result: [AnyObject] = managedObjectContext!.executeFetchRequest(fetchRequest, error:&error)!
return result
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return loadBooks().count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! UITableViewCell
var book: Book = loadBooks()[indexPath.row] as! Book
cell.textLabel!.text = book.title
return cell
}
#IBAction func addNew(sender: AnyObject) {
let entity = NSEntityDescription.entityForName("Book", inManagedObjectContext:managedObjectContext)
var book = Book(entity: entity!,insertIntoManagedObjectContext:managedObjectContext)
book.title = "My Book:" + String(loadBooks().count)
var error: NSError?
managedObjectContext.save(&error)
myTableView.reloadData() //mistake!
}
}
You have to declare your an outlet for your tableview which is probably set up in the according storyboards view controller.
import UIKit
import CoreData
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var managedObjectContext: NSManagedObjectContext!
#IBOutlet weak var myTableView: UITableView!
// your code
}
Please don't forget to connect your table view instance from the storyboard with your new created outlet, by opening code and storyboard side-by-side and draw a line from the dot next to the outlet to the tableview.
Or you can use the way apple describes here (https://developer.apple.com/library/ios/recipes/xcode_help-IB_connections/chapters/CreatingOutlet.html) to create your outlet from the table view on the storyboard.
edit:
Nevermind, i realized that if you wouldnt have had connected the dataSource and delegate, the crash would have happend way before the addNew().
Kie's answer is correct, however to see an actual result you also need to connect the dataSource and delegate of the tableView to your class.
You can do this either in storyboard, or in code in the viewDidLoad method.
override func viewDidLoad(){
super.viewDidLoad()
myTableView.dataSource = self
myTableView.delegate = self
// Additional code
}