NSOutlineView crash when isGroupItem delegate method is used with Swift - cocoa

I want to deploy Source List using NSOutlineView in a Swift project.
The view controller below works well when the isGroupItem delegate method is not invoked. However, many __NSMallocBlock__ items will be returned when the isGroupItem method is used. Which I have no idea where these items come from. The items I provided are only strings.
class ViewController: NSViewController, NSOutlineViewDataSource, NSOutlineViewDelegate {
let topLevel = ["1", "2"]
let secLevel = ["1": ["1.1", "1.2"], "2": ["2.1", "2.2"]]
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
if let str = item as? String {
let arr = secLevel[str]! as [String]
return arr.count
} else {
return topLevel.count
}
}
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
return outlineView.parentForItem(item) == nil
}
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
var output: String!
if let str = item as? String {
output = secLevel[str]![index]
} else {
output = topLevel[index]
}
return NSString(string: output)
}
func outlineView(outlineView: NSOutlineView, objectValueForTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?) -> AnyObject? {
return item
}
func outlineView(outlineView: NSOutlineView, isGroupItem item: AnyObject) -> Bool {
return (outlineView.parentForItem(item) == nil)
}
func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
return outlineView.makeViewWithIdentifier("HeaderCell", owner: self) as NSTextField
}
}
The sample project can be downloaded here

If you check out the NSOutlineView documentation you will see that it stores only pointers; it doesn't retain the objects returned from the child:ofItem: delegate method. So, when you do this line:
return NSString(string: output)
You are returning a new NSString instance that is quickly released (since the outline view does not retain it). After that point, anytime you ask questions about the items you will get a crash, because the NSString has been freed.
The solution is simple: store the NSStrings in an array and return those same instances each time.
corbin

This question has been answered by Ken Thomases in apple developer forum. Here extracted what he said:
The items you provide to the outline view must be persistent. Also, you have to return the same item each time for a given parent and index. You can't return objects that were created ad hoc, like you're doing in -outlineView:child:ofItem: where you call the NSString convenience constructor.
It works fine after persisting the datasource objects as follow:
let topLevel = [NSString(string: "1"), NSString(string: "2")]
let secLevel = ["1": [NSString(string: "1.1"), NSString(string: "1.2")], "2": [NSString(string: "2.1"), NSString(string: "2.2")]]
then return the stored NSString in the outlineView:child:ofItem: datasource method.

It's because NSOutlineView works with objects inherited from NSObject, and Swift string is incompatible type.

Related

Swift 3 Table Column Width Last Column Not Working

Refer to my attached image.
Notice the last column for some reason is always short on the width. I can't for the life of me figure out why or how to fix this?
Here is my code for my controller.
import Cocoa
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
#IBOutlet weak var theTableview: NSTableView!
var data:NSArray = [""] //#JA - This is used
override func viewDidLoad() {
super.viewDidLoad()
//First remove all columns
let columns = self.theTableview.tableColumns
columns.forEach {
self.theTableview.removeTableColumn($0)
}
//self.theTableview?.columnAutoresizingStyle = .sequentialColumnAutoresizingStyle
for index in 0...100 {
let column = NSTableColumn(identifier: "defaultheader")
if(index != 0){
column.title = "Month \(index)"
}else{
column.title = "Factors"
}
self.theTableview.addTableColumn(column)
}
// Do any additional setup after loading the view.
data = ["Group 1","Group 2","Group 3","Group 4"]
self.theTableview.reloadData()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func numberOfRows(in tableView: NSTableView) -> Int {
return data.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if let cell = tableView.make(withIdentifier: "defaultcell", owner: nil) as? NSTableCellView {
cell.textField?.stringValue = data.object(at: row) as! String
return cell
}
return nil
}
#IBAction func startsimulation(_ sender: NSButton) {
//Recalculates the data variable for updating the table.
data = ["group1","group2"]
theTableview.reloadData()
}
}
NSTableColumn has a property resizingMask and NSTableView has a property columnAutoresizingStyle. Both can be set in IB or in code. Figure out a configuration so the columns behave like you want. The default Column Sizing of the table view in IB is 'Last Column Only', switching to 'None' will fix your problem.
Another solution is setting minWidth of the columns.

NSTableview - Drag and Drop split into two classes and controllers

i found this helpfully tutorial for realize drag an drop with nstabelview:
https://drive.google.com/open?id=0B8PBtMQt9GdONzV3emZGQWUtdmM
this works fine.
but i would like to split both table views into differente view controllers and classes with a split view:
one split view controller:
item 1: viewcontroller with source nstableview (SourceTableView.class)
item 2: viewcontroller with target nstableview (TargetTableView.class)
how can i do this with this project?
i know how can i create a split view controller in storyboard.
but i dont know, if i have two different classes, how the iBoutlet SourceTabelView of class SourceTableView.class assign the iBoutlet TargetTableView of class TargetTableView.class
UPDATE
var person = [Person]()
NSManagedObject.class
import Foundation
import CoreData
#objc(Person)
public class Person: NSManagedObject {
#NSManaged public var firstName: String
#NSManaged public var secondName: String
}
Example of drag and drop between two table views inside a split view. Dragging inside one table view and multiple selection will work. Hold the Option key to drag a copy.
The datasource of each table view is the view controller inside the split view. Each table view has its own view controller and each view controller controls one table view. Both view controllers are the same NSViewController subclass:
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
#IBOutlet weak var myTableView: NSTableView!
var dataArray: NSMutableArray = ["John Doe", "Jane Doe", "Mary Jane"]
override func viewDidLoad() {
super.viewDidLoad()
myTableView.register(forDraggedTypes: ["com.yoursite.yourproject.yourstringstype"])
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
// NSTableViewDataSource data methods
func numberOfRows(in tableView: NSTableView) -> Int {
return dataArray.count
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return dataArray[row] as AnyObject!;
}
// NSTableViewDataSource drag methods
func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool {
// the dragging destination needs the strings of the rows to add to its own data,
// we, the dragging source, need the indexes of the rows to remove the dropped rows.
pboard.declareTypes(["com.yoursite.yourproject.yourstringstype", "com.yoursite.yourproject.yourindexestype"],
owner: nil)
pboard.setData(NSKeyedArchiver.archivedData(withRootObject: (dataArray as NSArray).objects(at:rowIndexes as IndexSet)), forType: "com.yoursite.yourproject.yourstringstype")
pboard.setData(NSKeyedArchiver.archivedData(withRootObject: rowIndexes), forType: "com.yoursite.yourproject.yourindexestype")
return true
}
func tableView(_ tableView: NSTableView, draggingSession session: NSDraggingSession,
endedAt screenPoint: NSPoint, operation: NSDragOperation) {
// remove the dragged rows if the rows are dragged to the trash or were moved to somewhere else.
var removeRows = false
if operation == .delete {
// trash
removeRows = true
} else if operation == .move {
// check if the point where the rows were dropped is inside our table view.
let windowRect = tableView.convert(tableView.bounds, to: nil)
let screenRect = view.window!.convertToScreen(windowRect)
if !NSPointInRect(screenPoint, screenRect) {
removeRows = true
}
}
if removeRows {
// remove the rows, the indexes are on the pasteboard
let data = session.draggingPasteboard.data(forType: "com.yoursite.yourproject.yourindexestype")!
let rowIndexes = NSKeyedUnarchiver.unarchiveObject(with: data) as! NSIndexSet
(dataArray as NSMutableArray).removeObjects(at: rowIndexes as IndexSet)
tableView.reloadData()
}
}
// NSTableViewDataSource drop methods
func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int,
proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation {
// only accept drop above rows, not on rows.
if dropOperation == .above {
// return move if the dragging source allows move
if info.draggingSourceOperationMask().contains(.move) {
return .move
}
// return copy if the dragging source allows copy
if info.draggingSourceOperationMask().contains(.copy) {
return .copy
}
}
return []
}
func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int,
dropOperation: NSTableViewDropOperation) -> Bool {
// if the rows were moved inside the same table view we do a reorder
var dropRow = row
if info.draggingSource() as AnyObject === myTableView as AnyObject &&
info.draggingSourceOperationMask().contains(.move) {
// remove the rows from their old position
let data = info.draggingPasteboard().data(forType: "com.yoursite.yourproject.yourindexestype")!
let rowIndexes = NSKeyedUnarchiver.unarchiveObject(with: data) as! NSIndexSet
(dataArray as NSMutableArray).removeObjects(at: rowIndexes as IndexSet)
// recalculate the row of the drop
dropRow -= rowIndexes.countOfIndexes(in: NSMakeRange(0, dropRow))
}
// insert the dragged rows
let data = info.draggingPasteboard().data(forType: "com.yoursite.yourproject.yourstringstype")!
let draggedStrings = NSKeyedUnarchiver.unarchiveObject(with: data) as! [Any]
dataArray.insert(draggedStrings, at:IndexSet(integersIn:dropRow..<(dropRow + draggedStrings.count)))
tableView.reloadData()
return true
}
}
To make dragging to the trash work, subclass NSTableView and override:
override func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor
context: NSDraggingContext) -> NSDragOperation {
let test = super.draggingSession(session, sourceOperationMaskFor: context)
Swift.print("sourceOperationMaskFor \(test)")
switch context {
case .withinApplication:
return [.move, .copy]
case .outsideApplication:
return [.delete]
}
}
p.s. I'm not familiar with Swift and had some trouble with arrays and indexsets so I used NSMutableArray and NSIndexSet.

Populating NSTableView from array in Swift (without XCode)

I'm trying to develop for OS X without the use of XCode in Swift. I'm running into extreme headaches trying to populate NSTableView from any kind of data source. Here is my code:
import Cocoa
class StupidDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
var lib:[NSDictionary] = [["a": "1", "b": "2"],
["a": "3", "b": "4"]]
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return lib.count
}
func tableView(tableView: NSTableView,
objectValueForTableColumn tableColumn: NSTableColumn?,
row: Int) -> AnyObject? {
let result = lib[row].objectForKey(tableColumn!.identifier)
return result
}
}
func make_table(window: NSWindow,
_ size:(x:Int, y:Int, width:Int, ht:Int),
_ title:String,
_ data:StupidDataSource
)-> NSTableView {
let tableContainer = NSScrollView(frame:NSMakeRect(0, 0, 400, 400))
let tableView = NSTableView(frame: NSMakeRect(0, 0, 400, 400))
tableView.setDataSource(data)
tableView.reloadData()
tableContainer.documentView = tableView
tableContainer.hasVerticalScroller = true
window.contentView!.addSubview(tableContainer)
return tableView
}
class AppDelegate: NSObject, NSApplicationDelegate {
let window = NSWindow()
func applicationDidFinishLaunching(aNotification: NSNotification)
{
set_window_args(window, 400, 400, "Ass")
let dumb_dict = StupidDataSource()
let tableytable = make_table(window, (100, 100, 0 ,0), "poop", dumb_dict)
window.makeKeyAndOrderFront(window)
window.level = 1
}
}
let app = NSApplication.sharedApplication()
app.setActivationPolicy(.Regular)
let controller = AppDelegate()
app.delegate = controller
app.run()
This can be run at the command line with "swift file.swift". I have implemented the methods required for NSTableViewDataSource, and initialized everything correctly according to Apple's documentation, yet nothing shows up in the table. What is missing?
I'm no expert but it looks to me as though your tableView function is just returning the contents of the relevant row from the dictionary.
I personally return a cell as an NSView? like:-
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView?
{
if let cell = myViewBasedTableView.makeViewWithIdentifier(tableColumn!.identifier, owner: nil) as? NSTableCellView
{
cell.textField!.stringValue = "Hello";
// Instead of "Hello". Put the contents of one element from your
// dict in here. This func is called automatically for every cell
// in the grid.
return cell;
}
return myEmptyDefaultCell;
}
Having said that maybe your example is correct for Cell Based tableView. I never used one of them so don't know. I always use viewBased table view. I often put little images/icons in the cells as well as text using code like:
cell.textField!.stringValue = "Hello";
cell.imageView!.image = smileyFace;
return cell;

Programmatically collapse a group row in NSOutlineView

I have an NSOutlineView with an action (see code) that collapse a row when the user clicks anywhere on that row. However it is not working for group.
Some rows are defined as group via the "shouldShowOutlineCellForItem" delegate method.
I can expand a group row programmatically, but not collapse it. Any suggestions?
isExpanded is correctly set via the notifications.
#IBAction func didClick(sender: AnyObject?)
{
assert(self.root != nil)
let selectedRow = outlineView.clickedRow
let proposedItem = (selectedRow == -1) ? self.root! : outlineView.itemAtRow(selectedRow) as! thOutlineNode
if proposedItem.isExpanded
{
self.outlineView.collapseItem(proposedItem)
}
else
{
self.outlineView.expandItem(proposedItem)
}
}
Possibly duplicate. Based on this existing SO question covering Objective-C, try adding the NSOutlineViewDelegate delegate method
func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: AnyObject) -> Bool {
return true
}
to the view controller of your NSOutlineView. From the Apple documentation for the NSOutlineViewDelegate, we see that this is expected behaviour:
optional func outlineView(_ outlineView: NSOutlineView,
shouldShowOutlineCellForItem item: AnyObject) -> Bool
...
Discussion
Returning NO causes frameOfOutlineCellAtRow: to return NSZeroRect,
hiding the cell. In addition, the row will not be collapsible by
keyboard shortcuts.

Show list of strings in Source List (NSOutlineView) in Swift

I'm trying to show a simple list of strings in a source list sidebar - similar to that in Finder or the Github app. From reading the protocol reference I can't see which method is setting what the view displays. So far I have:
var items: [String] = ["Item 1", "Item 2", "Item is an item", "Thing"]
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
return items[index]
}
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
return false
}
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
if item == nil {
return items.count
}
return 0
}
func outlineView(outlineView: NSOutlineView, objectValueForTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?) -> AnyObject? {
return "ITEM"
}
func outlineView(outlineView: NSOutlineView, setObjectValue object: AnyObject?, forTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?) {
println(object, tableColumn, item)
}
// Delegate
func outlineView(outlineView: NSOutlineView, dataCellForTableColumn tableColumn: NSTableColumn?, tem item: AnyObject) -> NSCell? {
println("Called")
let view = NSCell()
view.stringValue = item as String
return view
}
And all I get is a source list with four blank items (No text). Do I need to override another method from the NSOutlineViewDelegate to show the information?
If you're happy to use a view-based outline view, rather than a cell-based one, you can replace the delegate method outlineView:dataCellForTableColumn:item, with its view equivalent outlineView:viewForTableColumn:item:
func outlineView(outlineView: NSOutlineView,
viewForTableColumn tableColumn: NSTableColumn?,
item: AnyObject) -> NSView? {
var v = outlineView.makeViewWithIdentifier("DataCell", owner: self) as NSTableCellView
if let tf = v.textField {
tf.stringValue = item as String
}
return v
}
Note that the important call within this method is the NSTableView method makeViewWithIdentifier:owner:. The first argument to this method - the string DataCell - is the value of the identifier Interface Builder gives to the NSTableViewCell object that it automatically inserts into your NSOutlineView when you drag it onto the canvas. This object has a textField property, and an imageView; all you need to do is set the stringValue property of the textField to the value of item.

Resources