How do I access an ImageView within an NSTableCellView in Swift? - swift

I'm trying to programmatically populate the cells of a column with images already defined in an array (flags).
In IB I have an Image View directly within a Table Cell View. In the ViewController this is what I'm doing:
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let cellView = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView else { return nil }
if tableColumn?.title == "flag" {
cellView.imageView?.image = NSImage(named: flags[row]) // NOT WORKING
} else if tableColumn?.title == "country" {
cellView.textField?.stringValue = flags[row].uppercased()
}
return cellView
}
This results in the text field cells in the "country" column getting set fine, but each of the cells in the "flag" column all have the default image I set in IB--or nothing if I take that out.
According to the tutorials and StackOverflow posts I've looked through, it seems that I'm doing everything right--but obviously I've messed up something.

(In response to the screen cast shared in the comments)
Notice how that Image View (in the cell) is not referenced by anything (its "Referencing Outlets" section on the right is empty).
Your image view is a subview of its cell (as indicated by its position in the view heiarchy on the left side bar), so you could presumably access it using something like:
let imageView = cell.subViews.first(where: { $0 is NSImageView }) as! NSImageView
imageView.image = NSImage(named: flags[row])
...but that's obviously clunky.
Instead, you should select your cell, and drag from its imageView outlet, to the Image View. This will make a new reference, which will make cell.imageView non-nil.
Alternatively, I would suggest you just delete your cell, and create a new one from the "Image & Test Table Cell View" from the "Object Library". It'll have all the outlets hooked up already.

Related

Retain The State Of On Off Button In A UITableViewCell - Swift

I have been searching through many posts on SO but couldn't find an answer to this one.
I have a Table view listing various items. Each cell in the table view has a button that swaps an image around when clicked on, effectively working as an "on" or "off" button to show a user which items in the list they have selected. I have a variable inside my custom Cell Prototype class which stores a value of true or false which is updated every time the button is clicked on.
There is a "Done" button in the Table View that when tapped on calls an unwind Segue to go back to the first View Controller.
When the user taps on the Enter Table View button (on the first View Controller) to display the Table the buttons all go back to their default state, am guessing because each time the segue to the Table View Screen happens it creates a new instance of the Table to be displayed.
What I'm trying to achieve is that the state of the button (either on or off) is retained when going back into the Table screen. I've tried for a while sending an Integer value back from the TableCell Class (using a delegate) to the first View controller and then passing that value back into the Table View controller when the forward segue is called in order to have a "retained from the previous state value" that can be compared against when the cells are created to indicate if a button had been clicked or not. Couldn't get it to work though to save the state of the buttons. The list of Items in the Table will also change depending on what a user adds.
Some Screen shots and the code are below. The code hasn't got the delegate i was trying included (as it didn't work) but if its needed I will edit the post to include it. Any help would be hugely appreciated. Many Thanks!!
Code I have so far is:
//Main View Controller//
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
//segue to the Table View Screen
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "EnterTableView" {
let newTableView = segue.destination as! TableViewController
}
}
// Unwind Segue Called on Exit From Table View
#IBAction func unwindToMainViewController (_segue:UIStoryboardSegue) {
}
}
// Table View Controller //
class TableViewController: UITableViewController {
var dataArray = ["A", "B", "C", "D"]
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
//create one section for table
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//create number of rows based on the number of items in the dataArray set above
return dataArray.count
}
// an array that will contain all the cells
var cellArray = [UITableViewCell] ()
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//create a new cell based on the cell class "TableViewCell"
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell
// variable to contain a single element from the dataArray - updates every time a new cell is created
let dataArrayForCells = dataArray [indexPath.row]
//set cell label text to show the value from the dataArrayForCells
cell!.label.text = dataArrayForCells
cellArray.append(cell!)
return cell!
}
// Table Cell Class//
class TableViewCell: UITableViewCell {
#IBOutlet weak var label: UILabel! // label to hold display some text
#IBOutlet weak var buttonImage: UIImageView! //image for button
var buttonClicked = true //variable to contain weather the has been clicked
//if button is tapped on run the below
#IBAction func aButton(_ sender: Any) {
//if button is clicked is true, swap image to red ("on") button, set buttonClicked value to false
if buttonClicked {
buttonImage.image = #imageLiteral(resourceName: "Rec Button Red")
buttonClicked = false
}
// if buttonClicked value is false swap image to grey ("off") button set buttonClicked value back to true
else {
buttonImage.image = #imageLiteral(resourceName: "Rec Button Grey")
buttonClicked = true
}
}

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

NSTableView Custom Row View Caching

I've been customising my NSTableRowView using rowViewForRow, but this has led to a serious performance issue when scrolling as the rows are constantly redrawn.
I tried updating my code to only redraw if the row doesn't exist which gives much faster/smoother scrolling, but sometimes some views now display the wrong content.
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let identifier = NSUserInterfaceItemIdentifier("MyTableRowView")
var rowView = tableView.makeView(withIdentifier: identifier, owner: self) as? MyTableRowView
// does the row exist?
if rowView == nil {
// create the row view
rowView = MyTableRowView(frame: NSRect.zero)
// set the identifier
rowView?.identifier = identifier
}
return rowView
}
Any suggestions much appreciated!
Figured this out. The function was working fine, but one of my custom button views required setNeedsDisplay on isChecked to update the display. This has previously always worked because the row had been redrawn all the time.

macOS - How do I use checkbox in NSTableView?

I'm trying to put checkbox to the first column of a view based tableView. I dragged a check box button to the column, then the structure is like
Table View
First Column
Check
Check
Text Cell
Then in the tableView method of the view controller I'm doing
if identifier == "Check" {
let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Check"), owner: self) as! NSTableCellView
return cell
}
I get a run time error Could not cast value of type 'NSButton' to 'NSTableCellView', what is the correct way to do it?
It's pretty simple.
In a view based NSTableView you can use any view which inherits from NSView as table cell view. The default is NSTableCellView with a text field but it's also possible to use control objects like NSTextField or NSButton without any underlying custom view just by dragging them into the table view canvas.
The error occurs because you have to cast the created view to the proper type. If you are using a checkbox cast the view to NSButton.
Don't think in terms of cell, think in terms of view. I even recommend to name the variable as view rather than as cell.
The NSUserInterfaceItemIdentifier extension is the recommended pattern to define constants
extension NSUserInterfaceItemIdentifier {
static let check = NSUserInterfaceItemIdentifier(rawValue: "Check")
}
let view = tableView.makeView(withIdentifier: .check, owner: self) as! NSButton
NSTableCellView has a imageView and a textField outlet. You need a checkbox and you can add one. Subclass NSTableCellView and add an outlet 'checkBox'. In IB, change the class of the cell view, replace the textfield by a checkbox (or add a checkbox) and connect the 'checkBox' outlet. In tableView(_:viewFor:row:), use checkBox instead of textField.
extension NSUserInterfaceItemIdentifier {
static let lhsCellImage = NSUserInterfaceItemIdentifier("lhsCellImage")
static let lhsCellName = NSUserInterfaceItemIdentifier("lhsCellName")
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if tableColumn?.identifier == .lhsCellImage {
let cellView = tableView.makeView(withIdentifier: .lhsCellImage, owner: self) as! IDMainTableImageCellView
let imageNameStr = tableImageList[row]
let imageName = NSImage(named: imageNameStr as NSImage.Name)
cellView.tableCellImageView.image = imageName
return cellView
} else if tableColumn?.identifier == .lhsCellName {
let cellView = tableView.makeView(withIdentifier: .lhsCellName, owner: self) as! NSTableCellView
cellView.textField?.stringValue = tableDataList[row]
return cellView
} else{
return nil
}
}