I'm new to Swift and coding, only about a month in and I am trying to build some simple UItableViews to set up some CoreData attributes.
The structure of my CoreData entities is a many-to-many relationship between the Workouts Entity and the Exercises Entity. (I would post some images but I don't have a high enough rep!)
What I'm trying to achieve is a simple settings menu where users can create a Workout and then create a series of Exercises within that Workout by using tableViews with a navigationController at the top (just like the iOS settings menu)
Currently I've got it working so that you can add some Workouts and then you can go to the Excercises tableView to add some Exercises. However I haven't been able to do two things:
1) How can I ensure that when a user adds an Exercise that it is assigned to the correct Workout that they've selected from the previous tableView?
2) How can I ensure that the Exercise tableView only shows Exercises of the Workout that they've selected?
I've done a lot of reading around the subject and think the answer is something to do with the segue from Workouts to Exercises (to pass the workoutName to the Exercises tableView by using a delegate? And then using NSPredicate to limit the Exercises shown to the Workout selected?
I'm not 100% sure, but any help would be greatly appreciated!
Here's the code for the segue from Workouts to Exercises:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "excerciseMaster" {
let ExcerciseMasterTableViewController = segue.destinationViewController as UIViewController
let indexPath = tableView.indexPathForSelectedRow()!
let workout = workouts[indexPath.row]
let destinationTitle = workout.workoutName
ExcerciseMasterTableViewController.title = destinationTitle
}
}
Here's the code for my Exercises tableViewController:
import UIKit
import CoreData
class ExcerciseMasterTableViewController: UITableViewController {
// Create an empty array of Excercises
var excercises = [Excercises]()
// Retreive the managedObjectContext from AppDelegate
let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
override func viewDidLoad() {
super.viewDidLoad()
// Use optional binding to confirm the managedObjectContext
if let moc = self.managedObjectContext {
}
fetchExcercises()
}
func fetchExcercises() {
let fetchRequest = NSFetchRequest(entityName: "Excercises")
// Create a sort descriptor object that sorts on the "excerciseName"
// property of the Core Data object
let sortDescriptor = NSSortDescriptor(key: "excerciseName", ascending: true)
// Set the list of sort descriptors in the fetch request,
// so it includes the sort descriptor
fetchRequest.sortDescriptors = [sortDescriptor]
if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [Excercises] {
excercises = fetchResults
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// How many rows are there in this section?
// There's only 1 section, and it has a number of rows
// equal to the number of excercises, so return the count
return excercises.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("Excercise Cell", forIndexPath: indexPath) as UITableViewCell
// Get the Excercises for this index
let excercise = excercises[indexPath.row]
// Set the title of the cell to be the title of the excercise
cell.textLabel!.text = excercise.excerciseName
cell.detailTextLabel!.text = "\(excercise.sets)x\(excercise.reps)"
cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator
return cell
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if(editingStyle == .Delete ) {
// Find the Excercise object the user is trying to delete
let excerciseToDelete = excercises[indexPath.row]
// Delete it from the managedObjectContext
managedObjectContext?.deleteObject(excerciseToDelete)
// Refresh the table view to indicate that it's deleted
self.fetchExcercises()
// Tell the table view to animate out that row
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
save()
}
}
// MARK: UITableViewDelegate
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let excercise = excercises[indexPath.row]
}
let addExcerciseAlertViewTag = 0
let addExcerciseTextAlertViewTag = 1
#IBAction func addExcerciseButton(sender: AnyObject) {
var namePrompt = UIAlertController(title: "Add Excercise",
message: "Enter Name",
preferredStyle: .Alert)
var excerciseNameTextField: UITextField?
namePrompt.addTextFieldWithConfigurationHandler {
(textField) -> Void in
excerciseNameTextField = textField
textField.placeholder = "Title"
}
namePrompt.addAction(UIAlertAction(title: "Ok",
style: .Default,
handler: { (action) -> Void in
if let textField = excerciseNameTextField {
self.saveNewItem(textField.text, workoutName: "Workout A")
}
}))
self.presentViewController(namePrompt, animated: true, completion: nil)
}
func saveNewItem(excerciseName : String, workoutName: String) {
// Create the new excercise item
var newExcercise = Excercises.createExcerciseInManagedObjectContext(self.managedObjectContext!, name: excerciseName)
println(excerciseName)
println(workoutName)
// Update the array containing the table view row data
self.fetchExcercises()
// Animate in the new row
// Use Swift's find() function to figure out the index of the newExcercise
// after it's been added and sorted in our Excercises array
if let newExcerciseIndex = find(excercises, newExcercise) {
// Create an NSIndexPath from the newExcerciseIndex
let newExcerciseIndexPath = NSIndexPath(forRow: newExcerciseIndex, inSection: 0)
// Animate in the insertion of this row
tableView.insertRowsAtIndexPaths([ newExcerciseIndexPath ], withRowAnimation: .Automatic)
save()
}
}
func save() {
var error : NSError?
if(managedObjectContext!.save(&error) ) {
println(error?.localizedDescription)
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "excerciseSettings" {
let ExcerciseSettingsDetailViewController = segue.destinationViewController as UIViewController
let indexPath = tableView.indexPathForSelectedRow()!
let excercise = excercises[indexPath.row]
let destinationTitle = excercise.excerciseName
ExcerciseSettingsDetailViewController.title = destinationTitle
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
1.The segue is close. Your ExcerciseMasterTableViewController doesn't have a property named workout. You need to add
var workout:Workout!
to your ExcerciseMasterTableViewController.
Your seque should look more like this
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "excerciseMaster" {
let desitnationController = segue.destinationViewController as ExcerciseMasterTableViewController
let indexPath = tableView.indexPathForSelectedRow()!
let workout = workouts[indexPath.row]
destinationController.workout = workout
let destinationTitle = workout.workoutName
desitnationController.title = destinationTitle // Usually you would put this in the viewDidLoad method of the destinationController
}
}
Then in your when you add exercises in your ExcerciseMasterTableViewController simply set their workout property
workout.exercises = exercises // See note in #2
To make sure only the correct exercises are shown, in your viewDidLoad set your exercises array to workout.exercises. Note that workout.exercises should be an NSSet, so you either need to convert your set to an array, or have exercises be of type NSSet instead of an array.
Related
I am building an iOS application for my year 12 project and it entails users being able to record information about a bill they have received and having the ability to take a photo of the bill and to save it. I have used core data for my users data to be saved to. I have currently been able to get the photo taken by the user to be able to be seen on a seperate screen when the user selects a bill. Where I am having trouble is that the YouTube video I used has only shown me how to display only 1 specific photo in position zero, as shown on line 39. I need help in getting a different image being displayed dependent on what bill the user selects. For example, if a user taps a water bill, on the viewing screen, they will see a water bill. Then if the user taps a gas bill, on the viewing screen, they will see the gas bill. Currently, what is happening is regardless of whether the user selects the gas or water bill, a water bill is displayed. I have tried to explain this the best I can, if there are any other concerns, please let me know.
Thank you for your assistance
import UIKit
import CoreData
class ViewControllerViewing: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// Getting keyboard to retract
}
func fetchImage() -> [Bravo] {
var fetchingImage = [Bravo]()
let fetchRequest = NSFetchRequest<Bravo>(entityName: "Bravo")
do {
fetchingImage = try context.fetch(fetchRequest)
} catch {
print("Error while fetching the image")
}
return fetchingImage
}
// Outlets
#IBOutlet weak var imgDisplay: UIImageView!
var selectedImage: String?
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
#IBOutlet weak var lblBill: UILabel!
// Actions
#IBAction func btnDisplay(_ sender: Any) {
let arr = DataBaseHelper.shareInstance.getAllImages()
// Got to get the numbered one change dependent on what bill is pressed
self.imgDisplay.image = UIImage(data: arr[0].photo!) // Only position zero photo displays
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
// Screen before photo screen
Class CarViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number bills
return bills.count
}
// Editing function
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Selected Person
let bravo = self.bills[indexPath.row]
// Create alert
let alert = UIAlertController(title: "Edit Bill", message: "Edit Provider", preferredStyle: .alert)
alert.addTextField()
// Edit text feild to edit provider
let txtProvider = alert.textFields![0]
// Configure button handler
let saveButton = UIAlertAction(title: "Save", style: .default) {
(action) in
// Edit provider property
bravo.provider = txtProvider.text
// Save new data
do {
try self.context.save()
}
catch {
}
// Refetch data
self.fetchBravo()
}
// Add button
alert.addAction(saveButton)
// Show alert
self.present(alert, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//ensure the cell identifier has been labelled "cell"
// let bravob = self.bills[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
// Recieves information from core data
let display = self.bills[indexPath.row]
// Displays the provider in the title
cell.textLabel?.text = display.provider
// Displays the date in the subtitle
cell.detailTextLabel?.text = display.date
//How to add photo into tableview research
return cell
}
func fetchBravo() {
// Fetch data from core data to display in a tableview
do {
let request = Bravo.fetchRequest() as NSFetchRequest<Bravo>
// Set the filtering and sorting on the request This is the sorting method (For setting car filter, try to adjust here) Look at predecite https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html
let pred = NSPredicate(format: "category CONTAINS '0'")
request.predicate = pred
self.bills = try context.fetch(request)
// Sort descripter
let sort = NSSortDescriptor(key: "provider", ascending: true)
request.sortDescriptors = [sort]
DispatchQueue.main.async {
self.tblCar.reloadData()
}
}
catch {
}
}
// Swipe to delete function https://www.youtube.com/watch?v=gWurhFqTsPU&list=RDCMUC2D6eRvCeMtcF5OGHf1-trw&start_radio=1
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
// Create swipe action
let action = UIContextualAction(style: .destructive, title: "Delete") { (action, view, completionHandler) in
// Which bill to removes
let BravoRemove = self.bills[indexPath.row]
// Remove Bill
self.context.delete(BravoRemove)
// Save updated delete
do {
try self.context.save()
}
catch{
}
// Refetch new data
self.fetchBravo()
}
// Return swipe action
return UISwipeActionsConfiguration(actions: [action])
}
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var bills:[Bravo] = []
override func viewDidLoad() {
super.viewDidLoad()
tblCar.delegate = self
tblCar.dataSource = self
tblCar.reloadData()
// Do any additional setup after loading the view.
}
// Outlets
#IBOutlet weak var txtDate: UITextField!
#IBOutlet weak var txtBill: UITextField!
#IBOutlet weak var tblCar: UITableView!
#IBOutlet weak var segCategory: UISegmentedControl!
// Actions
#IBAction func btnSearch(_ sender: Any) {
// Re-Fetch data for core data
self.fetchBravo()
print(bills)
tblCar.reloadData()
}
I have created a custom tableViewCell class for a prototype cells which is holding a text field. Inside ThirteenthViewController, I would like to reference the tableViewCell class so that I can access its doorTextField property in order to assign to it data retrieved from UserDefaults. How can I do this?
class ThirteenthViewController: UIViewController,UITableViewDelegate,UITableViewDataSource,UITextFieldDelegate {
var options = [
Item(name:"Doorman",selected: false),
Item(name:"Lockbox",selected: false),
Item(name:"Hidden-Key",selected: false),
Item(name:"Other",selected: false)
]
let noteCell:NotesFieldUITableViewCell! = nil
#IBAction func nextButton(_ sender: Any) {
//save the value of textfield when button is touched
UserDefaults.standard.set(noteCell.doorTextField.text, forKey: textKey)
//if doorTextField is not empty assign value to FullData
guard let text = noteCell.doorTextField.text, text.isEmpty else {
FullData.finalEntryInstructions = noteCell.doorTextField.text!
return
}
FullData.finalEntryInstructions = "No"
}
override func viewDidLoad() {
let index:IndexPath = IndexPath(row:4,section:0)
let cell = tableView.cellForRow(at: index) as! NotesFieldUITableViewCell
self.tableView.delegate = self
self.tableView.dataSource = self
cell.doorTextField.delegate = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return options.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
// configure the cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
if indexPath.row < 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = options[indexPath.row].name
return cell
} else {
let othercell = tableView.dequeueReusableCell(withIdentifier: "textField") as! NotesFieldUITableViewCell
othercell.doorTextField.placeholder = "some text"
return othercell
}
}
}//end of class
class NotesFieldUITableViewCell: UITableViewCell {
#IBOutlet weak var doorTextField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
In order to access the UITextField in your cell, you need to know which row of the UITableView contains your cell. In your case, I believe the row is always the 4th one. So, you can create an IndexPath for the row and then, you can simply do something like this:
let ndx = IndexPath(row:3, section: 0)
let cell = table.cellForRow(at:ndx) as! NotesFieldUITableViewCell
let txt = cell.doorTextField.text
The above might not be totally syntactically correct since I didn't check for syntax, but I'm sure you can take it from there, right?
However, do note that in order for the above to work, the last row (row 4) has to be always visible. If you try to fetch rows which are not visible, you do run into issues with accessing them since UITableView reuses cells and instantiates cells for the visible rows of data.
Also, if you simply want to get the text that the user types and text input ends when they tap "Enter", you can always simply bypass accessing the table row at all and add a UITextFieldDelegate to your custom cell to send a notification out with the entered text so that you can listen for the notification and take some action.
But, as I mentioned above, this all depends on how you have things set up and what you are trying to achieve :)
Update:
Based on further information, it appears as if you want to do something with the text value when the nextButton method is called. If so, the following should (theoretically) do what you want:
#IBAction func nextButton(_ sender: Any) {
// Get the cell
let ndx = IndexPath(row:4, section: 0)
let cell = table.cellForRow(at:ndx) as! NotesFieldUITableViewCell
//save the value of textfield when button is touched
UserDefaults.standard.set(cell.doorTextField.text, forKey: textKey)
//if doorTextField is not empty assign value to FullData
guard let text = cell.doorTextField.text, text.isEmpty else {
FullData.finalEntryInstructions = cell.doorTextField.text!
return
}
FullData.finalEntryInstructions = "No"
}
You can create a tag for the doorTextField (for instance 111)
Now you can retrieve the value.
#IBAction func nextButton(_ sender: Any) {
//save the value of textfield when button is touched
guard let textField = self.tableViewview.viewWithTag(111) as! UITextField? else { return }
prit(textField.text)
.....
}
I working on a project that is written in swift 3.0. My requirement is to save data (on CoreData) that I enter on some text fields and populate one of those attributes in to a table view, thus once a row is selected I wants to update that record (re-assign values on my text fields and save).
Basically I have an entity named "Task" and it got three attributes, and I wants to populate one of those attributes(called "name") that I have saved on core data, in to a table view. Hence when I wants to edit the data that I entered, I tap on a row and it'll direct me to the ViewController where I initially entered those data. However when I click the back button without saving the data it'll duplicate the array and populate in my table view. how can I stop this. The code of the table view class as follow.
import UIKit
import CoreData
class TableViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var stores = [Store] ()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.reloadData()
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
let request = NSFetchRequest <NSFetchRequestResult> (entityName: "Store")
request.returnsObjectsAsFaults = false
do {
let results = try context.fetch(request) as! [Store]
// check data existance
if results.count>0 {
print(results.count)
for resultGot in results {
if let expName = resultGot.name {
print("expence name is :", expName)
stores += [resultGot]
print("my array is : \(stores)")
}
}
self.tableView.reloadData()
}
}catch{
print("No Data to load")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stores.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell ()
let store = stores [indexPath.row]
cell.textLabel?.text = store.name
//cell.textLabel?.text = myExpensesArray[indexPath.row]
return cell
}
#IBAction func nextButtonPressed(_ sender: AnyObject) {
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "editStore", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "editStore"{
let v = segue.destination as! ViewController
let indexPath = self.tableView.indexPathForSelectedRow
let row = indexPath?.row
v.store = stores[row!]
}
}
This is happening because already loaded elements are present inside your array. When you came back to previously loaded ViewController its method viewWillAppear, viewDidAppear called everytime according to the viewController's life cycle.
You need to clear your previously loaded array using removeAll() method when you came back.
Use below code:
override func viewDidAppear(_ animated: Bool) {
stores.removeAll() // clears all element
let request = NSFetchRequest <NSFetchRequestResult> (entityName: "Store")
request.returnsObjectsAsFaults = false
do {
let results = try context.fetch(request) as! [Store]
// check data existance
if results.count>0 {
print(results.count)
for resultGot in results {
if let expName = resultGot.name {
print("expence name is :", expName)
stores += [resultGot]
print("my array is : \(stores)")
}
}
self.tableView.reloadData()
}
}catch{
print("No Data to load")
}
}
You populate your tableView in the viewDidAppear method which is execute everytime the view is shown (either for first time or coming back from a anbother detail view controller).
You either
populate it once by moving the populate code to viewDidLoad
or clean (remove all objects from) the stores before repopulating it, if you need fresh data to be shown. so before for resultGot in results
insert something like
stores = []
I have two viewcontrollers, one is rootviewcontroller, the other one is selectorviewcontroller. In rootvc, there is a textfield and button, when the button is clicked, it takes us to the selectorvc where we can choose and if necessary add a new item (area) and then choose the item, after we choose it, it takes us back to the rootvc, and display the selected item in the textfield. I understand that if we don't use data persistence measures, the data added in won't persist after we recommence the app. Although I can add in new item to the selectorvc, but the newly added data just gone even after we unwind the segue back to rootvc and re-enter the selectorvc. I am not sure where I did wrong, as the data storing array is mutable. It is great if you could pointing me to the right direction. Thanks a lot.
A simple array is defined to store the data,
import UIKit
class AreaClass {
var areaName: String
init? (areaName: String) {
self.areaName = areaName
if areaName.isEmpty {
return nil
}
}
}
This is the unwind segue in the rootvc,
#IBAction func unwindWithSelectedArea(segue:UIStoryboardSegue) {
if let SelectorViewController = segue.sourceViewController as? SelectorViewController,
selectedArea = SelectorViewController.selectedArea
{
AreaSelectedTextField.text = selectedArea
}
}
This is the declaration and addnewitem in the selectorvc,
var selectedArea: String?
var selectedAreaIndex: Int?
var areas = [AreaClass]()
var newarea = AreaClass?()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
areaNewnSelectedTF.delegate = self
saveButton.enabled = false
loadSample()
// Do any additional setup after loading the view.
}
func loadSample (){
let area1 = AreaClass(areaName: "TopHill")!
let area2 = AreaClass(areaName: "Foothill")!
let area3 = AreaClass(areaName: "Summit")!
let area4 = AreaClass(areaName: "Riverside")!
areas += [area1, area2, area3, area4]
}
#IBAction func addNewArea(sender: UIBarButtonItem) {
var dupli = false
if saveButton == sender {
let areaname = areaNewnSelectedTF.text ?? ""
newarea = AreaClass(areaName: areaname)
for var index = 0; index < areas.count; ++index {
if areaname == areas[index].areaName {
dupli = true
// Mark: alert for duplicate inputs
let alert = UIAlertController(title: "Duplicate", message: "Can't have same items", preferredStyle:.Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
presentViewController(alert, animated: true, completion: nil)
}
}
if dupli == false {
let newIndexPath = NSIndexPath(forRow: areas.count, inSection: 0)
areas.append(newarea!)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
}
}
This is the PrepareforSegue in selectorvc
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "saveSelectionSegue" {
if let cell = sender as? UITableViewCell {
let indexPath = tableView.indexPathForCell(cell)
if let index = indexPath?.row {
selectedArea = areas[index].areaName
}
}
}
}
You need to use a delegate to pass data from a childViewController to your rootViewController.
Below there are ViewController(root) and SelectTableViewController(selector), they implement a simple example of what you look for. I used a struct to make it simple.
At first, you need to create a protocol using the role of your delegate => ViewControllerDelegate with a simple function with an argument "AreaStruct".
This protocol needs to be implemented by the "root". When you pressed the button, you performSegueWithIdentifier that will call prepareForSegue. In it, you pass yourself(ViewControllerDelegate) to the destinationViewController.
In your destinationViewController, the user select one row. By selecting, it triggers didSelectRowAtIndex. In it, you get the selectedArea (the concern AreaStruct) and with your delegate, you call choseArea(...). After that you pop the ViewController to go back to the root.
When you call choseArea, it will put the areaName into the label to display what you selected as shown by the implementation of choseArea(areaStruct : AreaStruct) in your RootViewController.(In this function, you can do whatever you want with your areaStruct)
------ EDIT ------
In your code, your "selectorviewcontroller" create the data => loadSample. So no matter what happened when you go to "selectorviewcontroller", you will always loadSample even if you added new "areas" before. I updated my example code based on your example.
ViewController -> press button -> SelectTableViewController -> add area -> select area -> back to ViewController
To summarise,
in ViewController, I setupListData when viewDidLoad is called
I pressed the button to go to SelectTableViewController
3 I passed myself(delegate) and dataList to SelectTableViewController in prepareForSegue
SelectTableViewController is loaded and display my list based on dataList !
I pressed addNewArea, it append a new area to "areas" that is only local to SelectTableViewController !
My list is refreshed and displays my new area
I select an area
I call the delegate by passing 2 arguments : What I selected and my updated areas(List); I pop SelectViewController => Popping SelectViewController, it disappear and all data are lost
When I called the delegate, I updated my label based on what I selected and also I updated dataList in ViewController with areas (from SelectTableViewController).
When I click on my button, see point 2.1. => You should understand
If you want to persist data, you could use different techniques such as singleton, core data, NSUserDefaults, etc.
ViewController.swift
import UIKit
import Foundation
struct AreaStruct {
var areaName : String
}
protocol ViewControllerDelegate : class{
func choseArea(areaStruct : AreaStruct, areas : [AreaStruct])
}
class ViewController: UIViewController, ViewControllerDelegate { //1. Implement the delegate here.
#IBOutlet weak var infoLabel: UILabel!
#IBOutlet weak var btnToVC: UIButton!
var dataList : [AreaStruct] = [AreaStruct]()
override func viewDidLoad() {
super.viewDidLoad()
self.setupListData()
}
func setupListData(){
for i in 0...5{
dataList.append(AreaStruct.init(areaName: "Coucou \(i)"))
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SelectTableViewController" {
let destinationViewController = segue.destinationViewController as! SelectTableViewController
destinationViewController.delegate = self //2. Here you passed itself to the destinationViewController so it can know how to call you !
destinationViewController.areas = dataList //
}
}
func choseArea(areaStruct : AreaStruct, areas : [AreaStruct]) {
self.infoLabel.text = areaStruct.areaName
dataList = areas
}
#IBAction func pushToSelectTableViewController(sender: AnyObject) {
//0. When pressed, you want to go to SelectTableViewController
self.performSegueWithIdentifier("SelectTableViewController", sender: nil)
}
}
SelectTableViewController.swift
import UIKit
class SelectTableViewController: UITableViewController {
var areas : [AreaStruct] = [AreaStruct]()
weak var delegate : ViewControllerDelegate? // <-- Delegate to send a mess. to ViewController
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addNewArea(sender: UIBarButtonItem) {
let hello_world = "Hello World" //For the example !
areas.append(AreaStruct.init(areaName: hello_world))
self.tableView.reloadData()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return areas.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
cell.textLabel?.text = areas[indexPath.row].areaName
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedArea = areas[indexPath.row]
self.delegate?.choseArea(selectedArea, areas: areas) //3. When you select, you pass the data to ViewController via the delegate
self.navigationController?.popViewControllerAnimated(true)//4. You dismiss itself
}
}
I’m implementing a search bar with an UISearchController within a sectioned table. So far so good.
The main issue is that when the the filtered results come along, it’s a whole new table with no sections and fewer rows.
When selecting the row, I perform a segue to that position in the array, but the detailed view is expecting that exact row or index from the main array, which I can’t get from the filtered array of objects, which may be [0] [1] [2] in 300 elements.
I guess I can compare the selected object with the main array and assuming there’s no duplicates, get the index from there and pass it over… But these seems pretty inefficient to me.
Apple does something similar (I unfortunately don’t know how) when filtering Contacts, in the Contacts App. How they pass the contact object? That’s pretty much my goal.
Here I let you a snippet of what I’m doing:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if(self.resultSearchController.active) {
customerAtIndex = indexPath.row // Issue here
performSegueWithIdentifier("showCustomer", sender: nil)
}
else {
customerAtIndex = returnPositionForThisIndexPath(indexPath, insideThisTable: tableView)
performSegueWithIdentifier("showCustomer", sender: nil)
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showCustomer" {
if let destination = segue.destinationViewController as? CustomerDetailViewController {
destination.newCustomer = false
destination.customer = self.customerList[customerAtIndex!]
destination.customerAtIndex = self.customerAtIndex!
destination.customerList = self.customerList
}
}
}
You can either do in another way, it a trick, but it works. First change your didSelectRowAtIndexPath as below:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var object :AnyObject?
if(self.resultSearchController.active) {
object = filteredArray[indexPath.row]
}
else {
object = self.customerList[indexPath.row]
}
performSegueWithIdentifier("showCustomer", sender: object)
}
Now, in prepareForSegue, get back the object and send it to your detailed view controller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showCustomer" {
if let destination = segue.destinationViewController as? CustomerDetailViewController {
destination.newCustomer = false
destination.customer = sender as! CustomerObject
destination.customerAtIndex = self.customerList.indexOfObject(destination.customer)
destination.customerList = self.customerList
}
}
}
Here's the trick I used in my code, I basically load the tableView from the filteredObjects array so then indexPath is always correct:
var selectedObject: Object?
private var searchController: UISearchController!
private var allObjects: [Object]? {
didSet {
filteredObjects = allObjects
}
}
private var filteredObjects: [Object]? {
didSet {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.tableView.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadData { objects in
self.allObjects = objects
}
}
// MARK:- UITableView
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredObjects?.count ?? 0
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
cell.textLabel?.text = filteredObjects?[indexPath.row].name
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedObject = filteredObjects?[indexPath.row]
}
// MARK:- UISearchBarDelegate
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
if !searchText.isEmpty {
filteredObjects = allObjects?.filter{ $0.name.lowercaseString.rangeOfString(searchText.lowercaseString) != nil }
} else {
filteredObjects = allObjects
}
Add a new property NSMutableArray *searchArray to your table view class and then pass all search results to this array in -(void)filterContentForSearchText:scope: method. After that you will be able to get the selected object self.searchArray[indexPath.row] in tableView:didSelectRowAtIndexPath:.
I see two solutions -
1) Why not make detailed view look for row or index in filtered array instead of main array. I guess you are concerned only about the object in that row that you want to use in detail.
2) Make each object in the array have a unique id. Pass the unique id on selection thru segue and let detailed view search(predicate) in main array for that id.