I'm a beginner to cocoa and I've been trying to make a simple app for Mac using swift programming language, but I'm stuck and can't find a solution.
I want to present a data from dictionary in two or more tableViews, where the first table will show key, and the second table will show value.
For example, I have a dictionary
var worldDict:NSDictionary = ["Africa":["Egypt", "Togo"],"Europe": ["Austria", "Spain"]]
I can present all continents in the first table, but I can't find out how to make second table to display countries from continent I choose in the first table.
My ViewController is a DataSource and Delegate for both tables.
extension ViewController: NSTableViewDataSource {
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
if tableView == continentTable {
return self.worldDict.valueForKey("Continent")!.count
} else if tableView == countryTable {
return self.worldDict.valueForKey("Continent")!.allKeys.count
}
return 0
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
var cell = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
if tableView == self.continentTable {
let continent: AnyObject? = wordlDict.valueForKey("Continent")
var keys = continent!.allKeys
cell.textField?.stringValue = keys[row] as! String
} else if tableView == self.countryTable {
var countriesOfContinent: AnyObject? = worldDict.valueForKey("Continent")?.valueForKey("Africa")!
cell.textField?.stringValue = countriesOfContinent?.allKeys[row] as! String
}
return cell
}
}
Here I present data from dictionary in tables, but separately, and can't figure out how to make them work together.
Also I know how to get the number of row that has been selected
extension ViewController: NSTableViewDelegate {
func tableViewSelectionDidChange(notification: NSNotification) {
let continentSelected = rowSelected()
}}
func rowSelected() -> Int? {
let selectedRow = self.continentTable.selectedRow
if selectedRow >= 0 && selectedRow < self.worldDict.valueForKey("Continent")!.count {
return selectedRow
}
return nil
}
Part of the problem is that you're relying on the ordering of the keys returned by allKeys() to be reliable, which it's not. You need to keep a separate array of continents. It can basically be a copy of whatever allKeys() returned on one occasion, but you should not keep calling allKeys() each time.
In numberOfRowsInTableView(), for the countries table, you want to return the number of countries in the selected continent:
} else if tableView == countryTable {
if let selectedContinentRow = rowSelected() {
let selectedContinent = continentsArray[selectedContinentRow]
return self.worldDict[selectedContinent].count
}
return 0
}
For tableView(_:viewForTableColumn:row:), you want to return an element from the selected continent's array of countries:
} else if tableView == self.countryTable {
if let selectedContinentRow = rowSelected() {
let selectedContinent = continentsArray[selectedContinentRow]
return self.worldDict[selectedContinent][row]
}
}
Also, whenever the selected continent changes, you need to tell the countries table to reload its data:
func tableViewSelectionDidChange(notification: NSNotification) {
// ... whatever else ...
let tableView = notification.object as! NSTableView
if tableView == continentTable {
countryTable.reloadData()
}
}
Related
I'm trying to display an image in a table view cell view on the condition of a Boolean value.
The Boolean is a representation of the state of an object of the class "Book" where the objects are initialized:
class Book: NSObject, Codable {
#objc dynamic var author: String
#objc dynamic var title: String
#objc dynamic var lentBy: String
#objc dynamic var available: Bool {
if lentBy == "" {
return true
} else {return false}
}
init(author: String, title: String, lentBy: String) {
self.author = author
self.title = title
self.lentBy = lentBy
}
}
If the String lentBy is not specified, the Bool available returns true: no one has lent the book and hence it should be available. Binding the available object to the table view, the respective table view cell displays either 1 or 0. Instead of 1 or 0 I would like it to display an image: NSStatusAvailable or NSStatusUnavailable.
Have a look at this: https://i.imgur.com/xkp0znT.png.
Where the text field "Geliehen von" (lent by) is empty, the status is 1 and should display the green circle; otherwise a red circle. The green circle you see now is simply dragged into the table cell view and is non-functional. But this is the idea.
Now I'm wondering how to display the respective image view instead of the Bool 1 or 0.
The table view is constructed with the interface builder in a storyboard. If I'm trying to make changes to it programmatically, nothing gets display in the table view anymore. I suppose this is due to the set bindings. Removing the bindings just for the last column doesn't work. This is how I tried it (without implementation of the image view; I don't know how to do that programmatically):
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if tableColumn == tableView.tableColumns[2] {
let cellIdentifier = "statusCellID"
let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: self) as? NSTextField
if let cell = cell {
cell.identifier = NSUserInterfaceItemIdentifier(rawValue: cellIdentifier)
cell.stringValue = books[row].lentBy
}
return cell
}
return nil
}
What's the best solution to achieve this? Could I somehow, instead of a Bool, directly return the respective, e.g. CGImage types for lentBys representation available?
You are using Cocoa Bindings. This makes it very easy.
In Interface Builder drag an NSTableCellView with image view into the last column and delete the current one.
Delete the text field and set appropriate constraints for the image view.
Rather than viewForColumn:Row implement
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return books[row]
}
Extend the model with an image property which is driven by KVO
class Book: NSObject, Codable {
#objc dynamic var author: String
#objc dynamic var title: String
#objc dynamic var lentBy: String
#objc dynamic var available: Bool {
return lentBy.isEmpty
}
#objc dynamic var image: NSImage {
return NSImage(named: (lentBy.isEmpty) ? NSImage.statusAvailableName : NSImage.statusUnavailableName)!
}
static func keyPathsForValuesAffectingImage() -> Set<String> { return ["lentBy"] }
init(author: String, title: String, lentBy: String) {
self.author = author
self.title = title
self.lentBy = lentBy
}
}
In Interface Builder bind the Value of the image view of the table cell view to Table Cell View > objectValue.image
I have table view like this (in a mac cocoa application):
In the leftmost panel you can see that I have set the identifier of the Table Cell View to "1". That's fine if you just have 2 columns, once the number goes up, this approach will become cumbersome. Can I do this programmatically?
Here is an example:
import Cocoa
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
private var dataModel = DataModel()
private var answer = 0
private var keyData: (Int, [Int]) = (0, []) {
didSet {
tbl.reloadData()
}
}
#IBOutlet weak var questionIndex: NSTextField!
#IBOutlet weak var tbl: NSTableView!
#IBAction func replay(_ sender: Any) {
dataModel = DataModel()
questionIndex.stringValue = "0:"
answer = 0
updateModel()
}
#IBAction func forward(_ sender: NSButton) {
if sender.tag == 1 {
answer += keyData.0
}
updateModel()
}
func updateModel() {
let group = dataModel.nextGroup()
if let g = group {
self.keyData = g
let s = questionIndex.stringValue
questionIndex.stringValue = String(Int(String(s.characters.dropLast()))! + 1) + ":"
return
}
let alert = NSAlert()
alert.messageText = "You did have \(answer) on your mind, didn't you?"
alert.runModal()
}
override func viewDidLoad() {
super.viewDidLoad()
for (n, col) in tbl.tableColumns.enumerated() {
col.identifier = String(n)
}
updateModel()
}
func numberOfRows(in tableView: NSTableView) -> Int {
return keyData.1.count / 8 + 1
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let colId = tableColumn!.identifier
let colIndex = Int(colId)!
let index = (row * 8) + colIndex
let cell = tbl.make(withIdentifier: colId, owner: self) as! NSTableCellView
if 0 <= index && index < keyData.1.count {
cell.textField!.integerValue = keyData.1[index]
} else {
cell.textField!.stringValue = ""
}
return cell
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
I have assigned the cell identifiers by hand, and made them identical the corresponding column index, so as to creating a mapping between the cell id and the 2D array (which is the underlying data model) column index. The app is running fine, I just don't like assigning these IDs by click-and-point.
The full project can be found here: https://github.com/kindlychung/MysteriousNum
Create custom cell and add init to it using following lines.
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
and register this cell class as.
self.tableView.register(CustomCell.self, forCellReuseIdentifier: "customCell")
also dequeueReusableCell using same cell like:
tableView.dequeueReusableCell(withIdentifier: "customCell",for: indexPath) as! CustomCell
I have a NSTableView with one column. I would like to print the row number of the row that the user has clicked on. I am not sure where I should start with this. Is there a method for this?
You can use the selectedRowIndexes property from the tableView in the tableViewSelectionDidChange method in your NSTableView delegate.
In this example, the tableView allows multiple selection.
Swift 3
func tableViewSelectionDidChange(_ notification: Notification) {
if let myTable = notification.object as? NSTableView {
// we create an [Int] array from the index set
let selected = myTable.selectedRowIndexes.map { Int($0) }
print(selected)
}
}
Swift 2
func tableViewSelectionDidChange(notification: NSNotification) {
var mySelectedRows = [Int]()
let myTableViewFromNotification = notification.object as! NSTableView
let indexes = myTableViewFromNotification.selectedRowIndexes
// we iterate over the indexes using `.indexGreaterThanIndex`
var index = indexes.firstIndex
while index != NSNotFound {
mySelectedRows.append(index)
index = indexes.indexGreaterThanIndex(index)
}
print(mySelectedRows)
}
Use -selectedRowIndexes
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSTableView_Class/#//apple_ref/occ/instp/NSTableView/selectedRowIndexes
Then you can use those indexes to grab the data from your dataSource
(typically an array)
I have a view-based NSTableView where all the cells are editable. I need to refresh the data from the model every time the user modifies a textField from the view.
All the doc I find is related to the cell-based NSTableView.
Does anyone have a clue about this?
EDIT:
I'm using data source to populate this NSTableView.
This is the code of the Controller of the NSTableView
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
#IBOutlet var globalView: NSView!
#IBOutlet var songsTableView: NSTableView!
var tableContents = NSMutableArray()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
for (song) in songManager.songs {
var obj = Dictionary<String,String>()
obj["title"] = song.title
obj["artist"] = song.artist
tableContents.addObject(obj)
}
songsTableView.reloadData()
}
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return tableContents.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView?{
var obj = tableContents[row] as Dictionary<String,String>
let column = tableColumn?.identifier
var cellView = tableView.makeViewWithIdentifier(column!, owner: self) as NSTableCellView
if column == "title" {
cellView.textField?.stringValue = obj["title"]!
}
if column == "artist" {
cellView.textField?.stringValue = obj["artist"]!
}
cellView.textField?.editable = true
return cellView
}
}
And this is the code of the class that manages the data.
var songManager = SongManager()
struct song {
var title = "No name"
var artist = "No artist"
}
class SongManager: NSObject {
var songs = [song]()
func addSong(title: String, artist: String) {
songs.append(song(title: title, artist: artist))
}
}
I have not touched the row that the storyboard creates by default, so I guess it contains a single NSTextField.
I get to display the data, but cannot detect when the user tried to modify a textfield.
Given how things are currently set up, the simplest approach is probably to connect the text field's action selector to an action method on a target, such as your controller. You can do that in IB or in your tableView(_:viewForTableColumn:row:) method.
In that action method, you can call songsTableView.rowForView(sender) to determine which row was edited. Each column's text field would have a different action method, such as changeTitle() or changeArtist(), so that you know which column was edited. (You could also use songsTableView.columnForView(sender), then get the table column by using the index in songsTableView.tableColumns[col], and checking the returned column's identifier. For that, you would assign specific identifiers to the columns rather than letting IB assign them automatically.)
Once you have the row, you look up your dictionary using var obj = tableContents[row] as Dictionary<String,String> and set the value for the key appropriate to the action method (or column identifier) to the sender's stringValue.
I am trying to reorder a my Core Data list when moving cells in "Edit Mode". There seems to be helpful discussion in this area for Obj-c (see links below), but I can't find anything in Swift.
Has anyone come across any related documentation for Swift? Or would anyone be willing to translate the obj-c code to Swift? Thanks!
Obj-c links:
How can I maintain display order in UITableView using Core Data?
How to implement re-ordering of CoreData records?
Here is a nice way I do it. It also checks if the destination may not contain any data and you can also move accross sections.
isMoving will inhibit the delegate from firing off. You also need a sortorder attribute at your entity.
private func indexIsOutOfRange(indexPath:NSIndexPath) -> Bool {
if indexPath.section > self.fetchedResultsController.sections!.count - 1 {
return true
}
if indexPath.row > self.fetchedResultsController.sections![indexPath.section].objects!.count - 1 {
return true
}
return false
}
override func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath)
{
if indexIsOutOfRange(destinationIndexPath) {
return
}
isMovingItem = true
if sourceIndexPath.section == destinationIndexPath.section {
if var todos = self.fetchedResultsController.sections![sourceIndexPath.section].objects {
let todo = todos[sourceIndexPath.row] as! FoodEntry
todos.removeAtIndex(sourceIndexPath.row)
todos.insert(todo, atIndex: destinationIndexPath.row)
var idx = 1
for todo in todos as! [FoodEntry] {
todo.sortOrder = NSNumber(integer: idx++)
}
try!managedObjectContext.save()
}
} else {
if var allObjectInSourceSection = fetchedResultsController.sections![sourceIndexPath.section].objects {
let object = allObjectInSourceSection[sourceIndexPath.row] as! FoodEntry
allObjectInSourceSection.removeAtIndex(sourceIndexPath.row)
for (index,object) in (allObjectInSourceSection as! [FoodEntry]).enumerate() {
object.sortOrder = NSNumber(integer: index)
}
if var allObjectInDestinationSection = fetchedResultsController.sections![destinationIndexPath.section].objects {
allObjectInDestinationSection.insert(object, atIndex: destinationIndexPath.row)
for (index,object) in (allObjectInDestinationSection as! [FoodEntry]).enumerate() {
object.sortOrder = NSNumber(integer: index)
object.section = NSNumber(integer: destinationIndexPath.section)
}
}
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
tableView.reloadRowsAtIndexPaths(tableView.indexPathsForVisibleRows!, withRowAnimation: .Fade)
})
isMovingItem = false
try!managedObjectContext.save()
fetch()
}