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.
Related
please help me to understand why the tableView.indexPathForSelectedRow method returns nil.
I want to make a transfer from Table View Controller to View Controller. I have a segue by a StoryBoard and leadingSwipeActions.
import UIKit
class TableViewController: UITableViewController {
let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return months.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = months[indexPath.row]
return cell
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "editItem" {
let path = tableView.indexPathForSelectedRow
print("\(path)") ##// Prints nil.##
}
}
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let editAction = UIContextualAction(style: .normal, title: "Edit Item") { [self] (action, view, completion) in
performSegue(withIdentifier: "editItem", sender: self)
}
editAction.backgroundColor = .systemOrange
return UISwipeActionsConfiguration(actions: [editAction])
}
}
Leading swipe - and subsequent tap of the action button - does not select the row.
Since the sender parameter of the "prepare for segue" method is defined as Any?, one approach would be to pass the indexPath when you call the segue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "editItem" {
// make sure an Index Path was passed as the sender
if let path = sender as? IndexPath {
print("Index Path: \(path)")
}
}
}
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
// note: use [weak self] to avoid possible retain cycle
let editAction = UIContextualAction(style: .normal, title: "Edit Item") { [weak self] (action, view, completion) in
guard let self = self else { return }
// pass the path as the sender
self.performSegue(withIdentifier: "editItem", sender: indexPath)
}
editAction.backgroundColor = .systemOrange
return UISwipeActionsConfiguration(actions: [editAction])
}
Edit - in response to comment
The sender: Any? parameter means that sender can be any object. That allows you to differentiate what action or piece of code initiated the segue.
Examples:
self.performSegue(withIdentifier: "editItem", sender: indexPath)
self.performSegue(withIdentifier: "editItem", sender: 1)
self.performSegue(withIdentifier: "editItem", sender: 2)
self.performSegue(withIdentifier: "editItem", sender: "Hello")
self.performSegue(withIdentifier: "editItem", sender: self)
self.performSegue(withIdentifier: "editItem", sender: myButton)
Then, in prepare(...), you can evaluate the sender to decide what to do next.
Quite often, when using table views to "push" to another controller that relates to the tapped cell (a "details" controller, for example), the developer will connect a segue from the cell prototype to Details view controller. Then (if you gave that segue an identifier of "fromCell"), you can do something like this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "fromCell" {
if let cell = sender as? UITableViewCell {
if let path = tableView.indexPath(for: cell) {
print("Index Path: \(path)")
// here we might get data for that row/section,
// and pass it to the next controller
}
}
}
}
In your case, we send the indexPath of the cell we just acted on as the sender parameter.
How do I populate the NSTableview with an array using reactive framework?
In iOS for UITableview:
self.viewModel.arrayElements.asObservable()
.observeOn(MainScheduler.instance)
.bind(to: detailsTableView.rx.items(cellIdentifier: "comment", cellType: UITableViewCell.self)){
(row,element,cell) in
cell.addSubview(cellView)
}.addDisposableTo(disposeBag)
how can i achieve the same for NSTableView
I ran into a similar need, and solved it with a BehaviorRelay (using RxSwift 5).
The BehaviorRelay acts as a mediator so it's possible to use regular NSTableViewDataSource and NSTableViewDelegate protocols
The important part is the self.detailsTableView.reloadData() statement which tells the tableview to reload the data, it is not triggered automatically.
Something like this:
var disposeBag = DisposeBag()
var tableDataRelay = BehaviorRelay(value: [Element]())
func viewDidLoad() {
viewModel.arrayElements.asObservable()
.observeOn(MainScheduler.instance)
.bind(to: tableDataRelay).disposed(by: disposeBag)
tableDataRelay
.observeOn(MainScheduler.instance)
.subscribe({ [weak self] evt in
self.detailsTableView.reloadData()
}).disposed(by: disposeBag)
}
func numberOfRows(in tableView: NSTableView) -> Int {
return tableDataRelay.value.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let element = tableDataRelay.value[row]
let cellView = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: nil) as? NSTableCellView
cellView?.textField?.stringValue = element.comment
return cellView
}
Try the below, you should use drivers not observables
read this https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Traits.md
import RxSwift
import RxCocoa
let data = Variable<[String]>([])
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
data.asDriver.drive( tableView.rx.items(cellIdentifier: "idenifier")){(row:Int, comment:String, cell:UITableViewCell) in
cell.title = report
}.disposed(by:bag)
}
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.
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.
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.