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

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.

Related

Protocol Doesn't Send Value to Other VC

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)

Any idea as to why my data isn't being passed to the new tableview?

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.

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)
}
}

pass data to previous view without segue,#IBaction swift

I have a View1 when i click on textbox i am going to view2(table view) to pick a value. I want to send the picked value to view1 for that textbox.
The controls are created programmatically so i am not using segue,IBActions.
I am trying to use the protocol methods still no success. Here is what i have tried.
class DynamicSuperView: UIViewController,UITableViewDelegate,UITableViewDataSource,dropdownDelegate,UITextFieldDelegate
{
func setValue(value:AnyObject)
{
print("dynamic view delegate method executed")
self.labelText = value as! String
//return selectedValue;
}
override func viewDidLoad() {
}
}
The second class is here with delegate method..
protocol dropdownDelegate {
func setValue(value: AnyObject);
}
class testClass: UIViewController,UITableViewDataSource,UITableViewDelegate,UISearchBarDelegate {
var delegate:dropdownDelegate! = nil
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vcName = names[indexPath.row]
print ("Table view cell clicked and value passed: \(vcName)")
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .checkmark
}
self.navigationController?.popViewController(animated: true)
delegate.setValue(value: "Testing delegate")
}
}
Once i select the value in the tableview i am calling the delegate method and trying to pass that value but no success.
I don't want to create the new instance of the previous view controller because i will lose the data already entered by the user, so i am popping the current view controller and going to the previous view controller.
Please suggest if my approach i correct or not?
ERROR:
fatal error: unexpectedly found nil while unwrapping an Optional value
Thank you in advance
Try this:
view func ButtonPressed() {
print("Button Pressed!!") // This will create the new instance of the view controller.
let vc = UIStoryboard(name:"Main", bundle:nil).instantiateViewController(withIdentifier: "Storage") as! testClass
vc.delegate = self
self.navigationController?.pushViewController(vc, animated:true)
}
You are not setting the delegate to first view controller.

Saving Tableview data in 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