Saving Tableview data in swift - swift

I'm currently working on a tableView based project in swift. I have two tableView running in my project. As my first tableView loaded with data which has a editActionsForRowAtIndexPath function to delete cell(as a Favourite) and moving it to my second tableview.I am using the following code.....
firstTableView:
var arrays = ["Alpha","Beta","Gamma","Phill","Below","Above","Clean",]
var deleted: [String] = []
//passing data to another tableVC
var sendSelectedData = NSString()
let defaults = NSUserDefaults.standardUserDefaults()
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let favorite = UITableViewRowAction(style: .Normal, title: "Favourite") { action, index in
print("favourite button tapped")
let editingStyle = UITableViewCellEditingStyle.Delete
if editingStyle == UITableViewCellEditingStyle.Delete {
self.tableView.beginUpdates()
self.deleted.append(self.arrays[indexPath.row])
self.arrays.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.endUpdates()
}
}
favorite.backgroundColor = UIColor.orangeColor()
return [favorite]
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "ShowDetails") {
// initialize new view controller and cast it as your view controller
let viewController = segue.destinationViewController as! favTableViewController
// your new view controller should have property that will store passed value
viewController.arrayx = deleted
}
}
Above code works as i planned. Every time, When the swipe action perform. that particular cell been moved to my second view controller.But, I am getting trouble saving data and reload tableview on both my first and second tableView Controller after tableViewCell been moved to my second tableView...
I know NSUserDefaults function will save data.But, I am not familiar using NSUserdefaults.So,Please someone point me the direction...
thanks in Advance...

NSUserdefaults should only be used for storing small amounts of data, ie: user defaults, and is not ideal for a table view, where in the real world you will have a lot of data to load and display.
Other native options are Coredata, Apple's database solution. Unfortunately it is a complex beast and a mere mortal such as I finds it a pain to use.
I highly recommend you investigate using Realm (https://realm.io/). It is a powerful database solution, well documented, and easy to use (and free!). If you want to learn how to integrate Realm into your project the following video will start you off with the absolute basics:
https://www.youtube.com/watch?v=qGXPqDX7dsw&nohtml5=False

Related

How to pass an image as a variable to UIImageView in separate View Controller?

I'm pulling in some JSON data and displaying it in a UITableView (iOS/iPhone 8). It displays the image, title, and description of each value. I've successfully got that down, however, are having trouble pulling the image into a separate View Controller.
By that, I mean, when a cell on the first View Controller is tapped, another view controller opens to display just the information from that cell.
I've been able to make the title and description accessible via a global variable and an indexPath. But the same won't apply to an image, due to a conflict with strings.
I've listed below what I have successfully done with the title and description strings and then show my proposition (which doesn't work of course).
How can I get an image that has already been loaded and is in an array, to be accessible like I already have with the title and description, for use in another View Controller?
The code that formats and gathers values from the JSON:
if let jsonData = myJson as? [String : Any] { // Dictionary
if let myResults = jsonData["articles"] as? [[String : Any]] {
// dump(myResults)
for value in myResults {
if let myTitle = value["title"] as? String {
// print(myTitle)
myNews.displayTitle = myTitle
}
if let myDesc = value["description"] as? String {
myNews.displayDesc = myDesc
}
if let mySrc = value["urlToImage"] as? String {
// print(mySrc)
myNews.src = mySrc
}
self.myTableViewDataSource.append(myNews)
// dump(self.myTableViewDataSource)
// GCD
DispatchQueue.main.async {
self.myTableView.reloadData()
}
}
}
}
Two variables I have outside of the class, in order to use them globally:
var petsIndex = ""
var petsDesc = ""
The code that works with the UITableView and its cells:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCell(withIdentifier: "reuseCell", for: indexPath)
let myImageView = myCell.viewWithTag(2) as! UIImageView
let myTitleLabel = myCell.viewWithTag(1) as! UILabel
myTitleLabel.text = myTableViewDataSource[indexPath.row].displayTitle
let myURL = myTableViewDataSource[indexPath.row].src
loadImage(url: myURL, to: myImageView)
return myCell
}
The code that I'm using to send the JSON values to another View Controller. I achieve this by utilizing those global variables:
// If a cell is selected, view transitions into a different view controller.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
petsIndex = self.myTableViewDataSource[indexPath.row].displayTitle
petsDesc = self.myTableViewDataSource[indexPath.row].displayDesc
// Img needs to go here
myIndex = indexPath.row
performSegue(withIdentifier: "segue", sender: self)
}
Here's what I was doing to maybe solve my problem. I converted the string to a UIImage? or a UIImageView? and passed it as data. I left the variable empty and would change it as the data came available. That would occur, when the cell was clicked. Then inside of the second View Controller, I would utilize a an IBOutlet for the UIImageView:
// Variable outside of the class
var petsImg: UIImageView? = nil
petsImg = self.myTableViewDataSource[indexPath.row].src
I'm stumped at this point. I have gotten errors about the image being a string and needed to be converted. And when it was converted, the variable always came back is empty or nil.
Update:
I just tried doing this. It works and doesn't throw any errors. However, I still get a value of nil
petsImg = UIImage(named: self.myTableViewDataSource[indexPath.row].src)
When you perform a segue, you can intercept the call so to prepare the view controller it is showing. The view of this controller at this point has not loaded yet, and so you will need to create properties inside your PostViewController; you could create properties for the title, description, and image.
However, it will be a lot easier passing this information around as your NewsInfo object, for example:
struct NewsInfo {
let displayTitle: String
let displayDesc: String
let src: String
var imageURL: URL { return URL(string: src)! }
}
As well as a custom cell class that takes a NewsInfo object as an argument to populate the outlets.
class NewsInfoTableViewCell: UITableViewCell {
var newsInfo: NewsInfo? {
didSet {
updateOutlets()
}
}
override func prepareForReuse() {
super.prepareForReuse()
newsInfo = nil
}
private func updateOutlets() {
textLabel?.text = newsInfo?.displayTitle
detailTextLabel?.text = newsInfo?.displayDesc
// loadImage()
}
}
Setup the custom table cell class and set the newsInfo property after dequeuing.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseCell", for: indexPath) as! NewsInfoTableViewCell
cell.newsInfo = myTableViewDataSource[indexPath.row]
return cell
}
When the cell is selected, you can pass it as the sender for performing the segue rather than setting the global variables to populate the PostViewController on viewDidLoad.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? NewsInfoTableViewCell {
performSegue(withIdentifier: "segue", sender: cell)
}
}
You can remove this whole method if you replace your existing segue by ctrl-dragging from the prototype cell to the PostViewControllerin IB to make the cell the sender of the segue.
We want this because we will intercept the segue to prepare the destination view controller by passing it the NewsInfo object of the cell that was selected and triggered the segue.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch (segue.destination) {
case (let controller as PostViewController, let cell as NewsInfoTableViewCell):
controller.newsInfo = cell.newsInfo
default:
break
}
}
Similar to how we pass a NewsInfo object to the cell to populate the outlets, you can do the same thing for the PostViewController.
Quick and dirty solution
In your second view controller load the image using:
// With petsImg being the url to your image:
if let url = URL(string: petsImg), let imageData = Data(contentsOf: url) {
imagePost.image = UIImage(data: data)
}
Proper solution
You should not work with global variables to pass data from one view controller to another (read why). Rather look at this question's answer to find out how to transfer data (in your case: the myNews object) from the table view to a detail view:
Send data from TableView to DetailView Swift
If this is too abstract, you can look at this tutorial. It covers what you want to do: https://www.raywenderlich.com/265-uisplitviewcontroller-tutorial-getting-started
It looks like your image is not an image but a url to an image. You can load images into image views using a library like nuke: https://github.com/kean/Nuke
Since network requests are called asynchronously, its a bit difficult to see at which point you're trying to configure your UIImageView. But once you have your network response you will do one of the two:
If your view controller is not yet loaded, (ie you load it once your network response is complete) you can configure the UIImageView in the prepare for sequel method.
If your view controller is already loaded (which is perfectly fine), you will need to set a reference to that view controller in the prepare for segue method. Then you can configure the view controller once the network request is made. I would make the reference to that VC weak, as the system (navigation stack) is already holding on to the VC strongly.
PS: I suggest you de-serialize your JSON response to an object. It will go a long way to help us understand your code. It's hard to see your issue when you're passing dictionary objects around. I suggest you use one of the following:
1. Codable protocol
2. ObjectMapper
3. MapCodableKit (this one is my library which I use personally)
PPS: I assumed you use storyboards.

How can I get the button title for each selected cell in a UICollectionView?

I have a collectionView of buttons as pictured below. I want to be able to select multiple of these cells, and in doing so pass the title of each selected button into an array of Strings.
UICollectionView - each cell has a button with a title
The UICollectionView is in WordViewController class
class WordViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout
And the UICollectionViewCell is in it's own file.
import UIKit
class WordCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var wordView: UIButton!
#IBAction func buttonTapped(_ sender: UIButton) {
if wordView.isSelected == true {
wordView.isSelected = false
wordView.backgroundColor = UIColor.blue
}else {
wordView.isSelected = true
wordView.backgroundColor = UIColor.green
}
}
}
I'm very new to Swift and I have been trying to find an answer to this for days, but I can't figure it out. I suspect I may have to use indexPathsForSelectedItems but I have tried this and can't get it working.
func indexSelected () {
let collectionView = self.collectionView
let indexPath = collectionView?.indexPathsForSelectedItems?.first
print(indexPath!)
let cell = collectionView?.cellForItem(at: indexPath!) as? WordCollectionViewCell
let data = cell?.wordView.currentTitle
print(data!)
}
I'm not sure if I have something fundamental wrong in the way I have set up my CollectionView or if it is something to do with me using buttons within the CollectionViewCells.
Any help would be very appreciated.
This is one way you could do it. First get the indexPaths for the selected cells. Then loop through the indexPaths and get your cell for each IndexPath (cast them as your custom CollectionViewCell to access your button). Now you can append each title to an array to save them.
var titleArray = [String]()
guard let selectedIndexPaths = collectionView.indexPathsForSelectedItems else { return }
for indexPath in selectedIndexPaths {
if let cell = collectionView.cellForItem(at: indexPath) as? WordCollectionViewCell {
self.titleArray.append(cell.yourButton.titleLabel?.text)
}
}
Welcome to SO. This sounds a bit like an X/Y problem: A case where you are asking about how to implement a specific (often sub-optimal) solution rather than asking about how to solve the problem in the first place.
You should not treat the views in your collection view as saving data. (buttons are views.)
You should use the button to figure out the indexPath of the cell the user tapped and then look up the information in your data model.
You should set up an array of structs (or an array of arrays, if your collection view is in sections of rows.) Each of those structs should contain the current settings for a cell.
Your collectionView(_:cellForItemAt:) method should use the array of structs to configure your sell for display.
As the user taps buttons and selects cells, you should update the struct(s) at the appropriate IndexPath(s) and then tell the collection view to update those cell.
If you need to do something with the selected cells, you should ask the collection view for an array of the selected cells, you should use those IndexPaths to index into your model array and fetch the struct for each IndexPath, and then look up teh data you need.
EDIT:
You can use a really simple extension to UICollectionView to find the indexPath of any view inside your collection view (and a button is a view, as mentioned...)
extension UICollectionView {
func indexPathForCellContaining( view: UIView) -> IndexPath? {
let viewCenter = self.convert(view.center, from: view.superview)
return self.indexPathForItem(at: viewCenter)
}
}

Xcode 8.3.3 Swift 3 - Variable not updating between files

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.

UISwitch in TableView in Switch

Hello fellow programmers! I have a challenge I need help with. I have built a table using a Custom Style Cell.
This cell simply has a Label and UISwitch. The label displays a name and the switch displays whether they are an Admin or not. This works perfectly. My challenge is how and where do I put code to react when the switch is changed.
So if I click the switch to change it from off to on where can I get it to print the persons name? If I can get the name to print I can do the php/sql code myself. Thanks and here is a snippet from my code.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as UITableViewCell
let admin = self.admin[indexPath.row]
let text1a = admin.FirstName
let text1aa = " "
let text1b = admin.LastName
let text1 = text1a + text1aa + text1b
(cell.contentView.viewWithTag(1) as UILabel).text = text1
if admin.admin == "yes" {
(cell.contentView.viewWithTag(2) as UISwitch).setOn(true, animated:true)
} else if admin.admin == "no" {
(cell.contentView.viewWithTag(2) as UISwitch).setOn(false, animated:true)
}
return cell
}
You have to set an action in your Custom Table View Cell to handle the change in your UISwitch and react to changes in it, see the following code :
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var label: UILabel!
#IBAction func statusChanged(sender: UISwitch) {
self.label.text = sender.on ? "On" : "Off"
}
}
The above example is just used to change the text of the UILabel regarding the state of the UISwitch, you have to change it in base your requirements of course. I hope this help you.
You need to listen .ValueChanged of UISwitch, in YOUR_CUSTOM_CELL to make some decisions. There you can catch to "println" your data.
Eric,
At some point in the tableview's lifecycle, you'll need to configure each UISwitch in the table cell with a target/action.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIControl_Class/index.html#//apple_ref/occ/instm/UIControl/addTarget:action:forControlEvents:
The action tells the UISwitch instance what method it should invoke when the switch is flipped by the user. The target tells the UISwitch instance what object is hosting that method.
Typically, you'll use the UITableViewController (or UIViewController) subclass as the target.

TableView Pushing to Detail View in Swift

I need help with pushing my tableView to a detailView. Right now I have most of the code, but right when my app loads, it goes to the detailedView, and whenever I try to go back to the tableView, it instantly goes back to the detailedView and the following error shows up in the debug area:
Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted.
I don't understand why this is happening since my code is not in the viewDidLoad section of the code... Here is my code for the tableView:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Label.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
cell.textLabel.text = Label[indexPath.row]
performSegueWithIdentifier ("showDetail", sender: self)
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "showDetail" {
}
}
return cell
}
Please leave an answer below if you can help me make my segue work how it is supposed to. Thanks!
P.S. I am also curious on how to customize the back button on a navigation bar from "< Back" to just "<".
P.S.S. How can I make the tableViewCell auto release after pressed so it doesn't stay highlighted? Just so it looks like a nicer app overall.
P.S.S.S Sorry, but one last thing is how can I customize the DeatilView based on what cell the user clicks on? I already have the new class set up with text, but not sure how to make the if statement that customizes the text for each cell clicked.
Thanks again!
The error is in your cellForRowAtIndexPath:
performSegueWithIdentifier ("showDetail", sender: self)
this triggers the segue when the cell is displayed, not when it's selected. The easiest way to fix that is to create a segue in IB from the cell to the destination view controller.
Also, this code:
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "showDetail" {
}
}
creates a function local to cellForRowAtIndexPath which is never executed. The correct way is to declare it in the class scope as an override of the superclass implementation.
If you have already defined a segue in IB, then your code should like:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
cell.textLabel.text = Label[indexPath.row]
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
}
}
you have to perform the segue in tableView(_:didSelectRowAtIndexPath:) not
tableView(_:cellForRowAtIndexPath:)
P.S. How to customize the back button on a navigation bar from "< Back" to just "<".
I found this old question has the answer in Objective C, at How to change text on a back button
You must set this in the View Controller you are segueing from.
Let's say we have a simple iPhone app, that has a UITableViewController with a bunch of cells listing off numbers, let's call it the "NumberListTVC". We embed this NumberListTVC in a NavigationController. We set a selection segue on one of the cells to another ViewController, which we'll call the "NumberDisplayerVC". This NumberDisplayerVC has one label, which we will set to the contents of the label that was clicked on the NumberList. So when you select a cell in NumberListTVC, it will push to the "NumberDisplayer" in the NavigationController.
So for this app, you would put the code to customize the back button in the NumberListTVC, so that when you are looking at the NumberDisplayerVC, it will show the back button we want it to, instead of the default (since the back button brings it back to the NumberListTVC).
In Objective-C, as per the linked answer, the code to change "< Back" to "<" would be:
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:#"" style:UIBarButtonItemStylePlain target:nil action:nil];
Basically, we create a new UIBarButtonItem with whatever title we want. In this case, I just gave it the title of "", which is just an empty String.
The same command in Swift would be:
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style:.Plain, target: nil, action: nil)
I put this in NumberListTVC's viewDidLoad method, to make sure it was ran when NumberListTVC loaded.
P.S.S. How can I make the tableViewCell auto release after pressed so it doesn't stay highlighted?
This is covered in Objective-C on the answer Selected UItableViewCell staying blue when selected
You just have to tell the UITableViewCell to deselect it in its didSelectRowAtIndexPath.
In Objective-C, you would override it and do this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//Do whatever else you want to do here...
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
The Swift code would be similar:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
//Do whatever else you want to do here...
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
P.S.S.S How can I customize the DetailView based on what cell the user clicks on?
For simplicity's sake, I am going to use my first example which is just using our navigationController with the NumberListTVC and NumberDisplayerVC, so not on a UISplitView, but the question asked affects either scenario.
Basically, you have to get what you want from the cell that was clicked on, and then set it to whatever you want to use it in the next ViewController. So, in your prepareForSegue, you would do something like this:
if segue.identifier == "amazingSegue"
{
if let cellText = (sender as? UITableViewCell)?.textLabel.text
{
if let someVC = segue.destinationViewController as? NumberDisplayerVC
{
someVC.textToDisplay = cellText
}
}
}
This is basically what Antonio said in his answer. I added a bit more to it. So in my case, the NumberListTVC is a bunch of dynamically created UITableViewCells. I want to grab the label out of each one and then use that in the NumberDisplayerVC. So we:
Check if it is the segue we want to actually work with, in this case the segue between NumberListTVC and NumberDisplayerVC is named "amazing segue".
Next, use optional binding to assign the text of the sender cell to the constant cellText. We use the optional type cast operator to check if the sender is a UITableViewCell. We then use optional chaining to look inside this UITableViewCell and extract the text from its textLabel.
Again use Optional Binding to assign our destinationViewController to the constant someVC. We optionally type cast it to a NumberDisplayerVC, which is what it should be. If it indeed is one, it will correctly bind to the someVC variable.
Then just set the textToDisplay property to the cellText constant we optionally bound earlier.
I'm not sure if this is the best way, I don't feel great about the tower of if let statements, but it does the job initially.