Access data from table view cell in Swift - xcode

I'm having trouble accessing an optional string in swift from a table view cell. Each cell has a title label as well as a detail text label and the amount of cells at any given time in the table view is dependent on the user (the table view is used to display saved values). Since the strings saved as the detail text label's text in each cell are way too long to read in the table view, I wish to load them in another view that is segued to when a cell is pressed. This is my prepare for segue method:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "cellSegue"
{
let cell = tableView.cellForRowAtIndexPath(tableView.indexPathForSelectedRow()!)
var transfer : ExplanationView = segue.destinationViewController as ExplanationView
if let unwrapped = cell?.textLabel?.text!
{
transfer.infoText.text = unwrapped
}
}
}
When a segue is performed, I get a fatal crash, the offending line is "transfer.infoText.text = unwrapped" and Xcode tells me it found nil when unwrapping an optional. During my debuggingg efforts, I've tried "println(cell?.textLabel?.text!)" and Xcode prints "Optional(String)" where "String" is the actual string I'm trying to access so it seems like I'm on the right track, but obviously there's something I'm missing. I've also tried "cell?.textLabel?.text" but I get the same error. Any help is appreciated

You shouldn't ever extract data from a view. You should extract data from a model. The fact that you have a tableView with cells implies you have a model for the data that goes in those cells. It might be an array of strings or whatever. Instead of trying to extract the data from the cells, just get the data from the data model you used to populate the cells. Something like:
let selectedIndexPath = tableView.indexPathForSelectedRow()
let selectedData = data[selectedIndexPath.row]

Your problem is not unwrapped but instead transfer.infoText.text. Your IBOutlets are not set up at the time of the prepareForSegue. You need add a property (var) to your destination view controller to hold the unwrapped value. Then in viewDidLoad when the IBOutlets are set up, copy the string to your text field.

Related

Xcode UITest scrolling to the bottom of an UITableView

I am writing an UI test case, in which I need to perform an action, and then on the current page, scroll the only UITableView to the bottom to check if specific text shows up inside the last cell in the UITableView.
Right now the only way I can think of is to scroll it using app.tables.cells.element(boundBy: 0).swipeUp(), but if there are too many cells, it doesn't scroll all the way to the bottom. And the number of cells in the UITableView is not always the same, I cannot swipe up more than once because there might be only one cell in the table.
One way you could go about this is by getting the last cell from the tableView. Then, run a while loop that scrolls and checks to see if the cell isHittable between each scroll. Once it's determined that isHittable == true, the element can then be asserted against.
https://developer.apple.com/documentation/xctest/xcuielement/1500561-ishittable
It would look something like this (Swift answer):
In your XCTestCase file, write a query to identify the table. Then, a subsequent query to identify the last cell.
let tableView = app.descendants(matching: .table).firstMatch
guard let lastCell = tableView.cells.allElementsBoundByIndex.last else { return }
Use a while loop to determine whether or not the cell isHittable/is on screen. Note: isHittable relies on the cell's userInteractionEnabled property being set to true
//Add in a count, so that the loop can escape if it's scrolled too many times
let MAX_SCROLLS = 10
var count = 0
while lastCell.isHittable == false && count < MAX_SCROLLS {
apps.swipeUp()
count += 1
}
Check the cell's text using the label property, and compare it against the expected text.
//If there is only one label within the cell
let textInLastCell = lastCell.descendants(matching: .staticText).firstMatch
XCTAssertTrue(textInLastCell.label == "Expected Text" && textInLastCell.isHittable)
Blaines answer lead me to dig a little bit more into this topic and I found a different solution that worked for me:
func testTheTest() {
let app = XCUIApplication()
app.launch()
// Opens a menu in my app which contains the table view
app.buttons["openMenu"].tap()
// Get a handle for the tableView
let listpagetableviewTable = app.tables["myTableView"]
// Get a handle for the not yet existing cell by its content text
let cell = listpagetableviewTable.staticTexts["This text is from the cell"]
// Swipe down until it is visible
while !cell.exists {
app.swipeUp()
}
// Interact with it when visible
cell.tap()
}
One thing I had to do for this in order to work is set isAccessibilityElement to true and also assign accessibilityLabel as a String to the table view so it can be queried by it within the test code.
This might not be best practice but for what I could see in my test it works very well. I don't know how it would work when the cell has no text, one might be able to reference the cell(which is not really directly referenced here) by an image view or something else. It's obviously missing the counter from Blaines answer but I left it out for simplicity reasons.

Xcode9 Swift4.2 NSOutlineView NSTreeController data

I am trying to create an outlineview in a MacOS app that has mutliple levels that are summaries for a set of data held in SQLite3. I have an outlineview working with a treecontroller with a very simple NSMutuableDictionary based on a model class.
import Cocoa
class Summary: NSObject {
#objc dynamic var name: String
#objc dynamic var trades: Int
#objc dynamic var avgPL: Double
#objc dynamic var pandl: Double
#objc dynamic var parent: String
#objc dynamic var isLeaf: Bool
#objc dynamic var childCount: Int
#objc dynamic var children: [Summary] = []
init(name: String, trades: Int, avgPL: Double, pandl: Double, parent: String, isLeaf: Bool,childCount: Int) {
self.name = name
self.trades = trades
self.avgPL = avgPL
self.pandl = pandl
self.parent = parent
self.isLeaf = isLeaf
self.childCount = childCount
}
#objc func add(child: Summary) {
children.append(child)
}
}
My simple example data is:
let root: [String : Any] = ["name": "Overall","trades":5,"avgPL":200,"pandl":500,"parent":"","isLeaf": false,"childCount": 2 ]
let dict: NSMutableDictionary = NSMutableDictionary(dictionary: root)
let l2a = Summary(name: "L2a", trades: 3, avgPL: 100, pandl: 300, parent: "L1",isLeaf: true,childCount: 0)
let l2b = Summary(name: "L2b", trades: 2, avgPL: 100, pandl: 200, parent: "L1",isLeaf: true,childCount: 0)
dict.setObject([l2a,l2b], forKey: "children" as NSCopying)
I pass the dictionary to the treeController:
treeController.addObject(dict)
And that works nicely giving me a collapsible outline:
But I have no idea how to add more levels or children to the children. I want to have up to four levels deep in the outline. I have all the SQL summaries working and I have tried so many variations of populating arrays and trying to create a dictionary with the data to no avail. I have children and childCount and isLeaf set on everything but treecontroller does not like the array complaining that isLeaf is not KVO compliant. My data in an array looks like this (not all of the data but enough to see what I'm doing) The main level and all of the subsequent children are all based on the Summary model class above. Can I simply convert this array to a dictionary? Or, can I make it KVO compliant by adding keys to the model class or something? I have all of the 4 levels in separate arrays I use to build the resultant array if that is useful :
I should add that I have an NSObject defined as an NSMutableArray and its content tied to the treeController. My treeController is bound to each variable in the model class and at the top level has:
If I pass the array I have built to the treeController I get the following error:
Failed to set (contentViewController) user defined inspected property on (NSWindow): [<_TtGCs23_ContiguousArrayStorageC11outlinetest7Summary_ 0x604000445160> addObserver:forKeyPath:options:context:] is not supported. Key path: isLeaf
After building out my NSOutlineView without an NSTreeController and getting everything working I still wanted to get back to this and implement the treeController in order to take advantage of the sorting mechanism it provides. And I did find as per my last comment that I did have something wrong in InterfaceBuilder that was causing it to complain about KVO compliance. I had everything wired correctly except for the Content Array binding on the treeController. Here I bound it to my ViewController and added my data array reportSummary to the Model Key Path.
I also no longer needed to manually add my data array to the treeController using treeController.addObject(reportSummary). Once this was working I was then able to implement sorting and everything is working well. I should point out two things.
Setup of sorting on the TreeController is slightly different than on an ArrayController tied to a TableView. With the tableview it was sufficient to specify which columns are sortable in the identity inspector in IB. But in the outlineView scenario I also needed to setup bindings in IB to the treeController and change the Controller Key from arrangedObjects to sortDescriptors.
While testing my tree controlled outlineview I ran into a problem when I double-clicked on a summary row. I had implemented Double Action on the outlineView in IB in order to control the expanding and collapsing of summary sections. Note that I read about doing this in a thread here and someone mentioned that you would need to maintain multiple arrays and track indexes because once a row is collapsed or expanded that changes the row number of all the subsequent rows. But I figured out that the solution is simply to iterate through rows in reverse order and expand or collapse them working back up the tree starting from outlineView.numberOfRows-1. This works well and along with Double Action (clicking) to expand and collapse I also added an NSSlider which tracks to the expansion level and lets me collapse all the lowest levels moving back up the tree instead of clicking all of the little arrows on each row. This broke when I implemented the treeController. I received an error
Could not cast value of type 'NSKVONotifying_NSTreeControllerTreeNode'
This line of code was the problem
let summary = reportOutline.item(atRow: x) as! Summary
I had to change this to
let node = reportOutline.item(atRow: x) as! NSTreeNode
let summary = node.representedObject as! Summary
And that is it. Working beautifully now.

NSTextField Loses Focus When Neighboring NSTableView Reloaded

I have a search field (NSTextField) called searchField and when you type in it, it refreshes the data shown in a NSTableView. The problem is that this refresh also triggers the selection of a table row, and that takes the focus out of the NSTextField.
The user types in the search field:
func controlTextDidChange(_ obj: Notification) {
if let field = obj.object as? NSTextField, field == searchField{
refreshData()
}
}
Then the NSTableView gets reloaded here:
func refreshData(){
//Process search term and filter data array
//...
//Restore previously selected row (if available)
let index = tableView.selectedRow
tableView.reloadData()
//Select the previously selected row
tableView.selectRowIndexes(NSIndexSet(index: index) as IndexSet, byExtendingSelection: false)
}
If I comment-out both the reloadData() and the selectRowIndexes then the search field behaves as intended (I can keep typing and it keeps the focus in the field). But if include either or both of those methods, the search field loses focus after I type the first character and refreshData() is called.
How can I keep focus in my search field and not let the table reload hijack the focus?
Ugh... it turns out I could never get the NSTableView to let go of the focus because it wasn't set to allow an Empty selection state. Checking a box in Interface Builder fixed it.

xcode 7.3 reference icon from assets folder for prototype table view cell

In Xcode 7 with swift, I'm trying to use icons from my assets folder to display in the icon slot in a normal prototype tableview cell.
My code compiles and the app runs fine, displaying all the proper row text titles in the table, but all the cells use an icon that I (perhaps stupidly) entered into the attributes inspector for the prototype table cell.
By default, the style=Basic table cell does not have an icon to the left of the Title in the storyboard. BUT... if you add the name of an icon image from the assets folder in the image=MyAssetsIconName, the UI builder automatically adds a UIImageView to the left end of the table row cell. And of course all rows display the image that you selected. Which is what my app currently does.
But I would like to change the icon image for each row, so that the image on any row matches the text rows that I display. So I tried to assign a new image (from the assets folder) to each row cell as it was created. I did the assignment right after I assigned the text title to the cell.
Here is my code, which runs fine (but doesn't display the images that I want).
class TsbReportsCell: UITableViewCell {
#IBOutlet weak var Title: UILabel!
#IBOutlet weak var Detail: UILabel!
#IBOutlet weak var Icon: UIImageView!
...
}
In the table view controller:
In the table view controller class:
// define datasources
var reportnames = [String]()
var reportimages = [String]()
override func viewDidLoad() {
super.viewDidLoad()
reportnames = ["Balance Report",
"Routine Report",
"Low Quality Report"]
reportimages = ["enter.png",
"exterior.png",
"export.png"]
}
override func tableView(tableView:.... {
// all this code works fine
let cell = tableView.dequeueReusableCellWithIdentifier...
let row = indexPath.row
cell.Title.text = reportnames[row]
// here are the lines that seem to have no effect at all
let rowimage = UIImage(named: reportimages[row])
cell.Icon = UIImageView(image: rowimage)
return cell
}
Any ideas what I'm doing wrong? I named an image in the attributes for the table cell row just to get the UI builder to add the imageview placeholder for me. But maybe I'm not allowed to ever override that image, even though I can create an outlet for it.
This seems so simple a problem, but I searched at least 20 posts, the net, the doc, and I still couldn't find anything. Thanks
It's very simple. You are saying:
cell.Icon = UIImageView(image: rowimage)
But that does not put this image view into the interface! It merely assigns it to the cell's property.
Instead, you need to have an image view in the cell and hook an outlet from the cell's Icon property to that image view. As a result, cell.Icon is the image view in the interface. Now you say
cell.Icon.image = rowimage
Man, are you ever fast at answering questions, Matt. I'm sure I'm not the only one who appreciates that.
It's funny how taking a half hour to write up a decent question for this site makes you start thinking about other things to try. I took a seg fault on one of my tries, and was poking around in the error messages. From them, I got the idea that an ImageView was a structure that had an "image" field in it (or something like that), so I tried some UIImageView x UIImage crosses to see what would happen.
Sure enough, I was trying to assign an ImageView to an Image (I think; I could still be wrong). Here's the code line that worked for me.
cell.Icon.image = UIImage(named: reportimages[row])
So you can see I'm assigning an image to an image here, which works properly. AND... it must be right, because it's the same code as Matt's!! :-)

Xcode with Swift: How can I pass object back and forth in wizard steps?

I am stuck on implementing a rather simple task.
I have wizard with 3 steps in which you create an entity of course.
The wizard consists of a navigation view followed by 3 regular view.
In the contoller of the first step I create the course object and pass it to next step via the segue event:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "Step1_To_Step2") {
let step2Controller = segue.destinationViewController as Step2Controller
step2Controller.course = self.course
}
}
What I can't figure out though is:
How can I pass the object when going the other way, from step 2 to step 1, for exmaple?
Is it good practice, memory-wise to replace the whole object each time or just specific fields within? (The object may contain an image or two)
Please check some attached images. It may help you to understand delegate if you not already aware of it. Attached first image is for segue and second for delegate.
Segue
Result

Resources