NSOutlineView, using item: AnyObject - swift

I'm creating a NSOutlineView. When implementing the Data Source, although I'm able to create the top hierarchy I can not implement the childHierarchy. The reason is that I can't read the item: AnyObject? which prevents me from returning the right array from the dictionary.
//MARK: NSOutlineView
var outlineTopHierarchy = ["COLLECT", "REVIEW", "PROJECTS", "AREAS"]
var outlineContents = ["COLLECT":["a","b"], "REVIEW":["c","d"],"PROJECTS":["e","f"],"AREAS":["g","h"]]
//Get the children for item
func childrenForItem (itemPassed : AnyObject?) -> Array<String>{
var childrenResult = Array<String>()
if(itemPassed == nil){ //If no item passed we return the highest level of hirarchy
childrenResult = outlineTopHierarchy
}else{
//ISSUE HERE:
//NEED TO FIND ITS TITLE to call the correct child
childrenResult = outlineContents["COLLECT"]! //FAKED, should be showing the top hierarchy item so I could return the right data
}
return childrenResult
}
//Data source
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject{
return childrenForItem(item)[index]
}
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool{
if(outlineView.parentForItem(item) == nil){
return true
}else{
return false
}
}
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int{
return childrenForItem(item).count
}
func outlineView(outlineView: NSOutlineView, viewForTableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
// For the groups, we just return a regular text view.
if (outlineTopHierarchy.contains(item as! String)) {
let resultTextField = outlineView.makeViewWithIdentifier("HeaderCell", owner: self) as! NSTableCellView
resultTextField.textField!.stringValue = item as! String
return resultTextField
}else{
// The cell is setup in IB. The textField and imageView outlets are properly setup.
let resultTextField = outlineView.makeViewWithIdentifier("DataCell", owner: self) as! NSTableCellView
resultTextField.textField!.stringValue = item as! String
return resultTextField
}
}
}
I used this as a reference, although it's Objective-C implemented

You need to cast the item to the correct type for your outline. Generally you'd want to use a real data model, but for your toy problem with exactly two levels in the hierarchy, this suffices:
func childrenForItem (itemPassed : AnyObject?) -> Array<String>{
if let item = itemPassed {
let item = item as! String
return outlineContents[item]!
} else {
return outlineTopHierarchy
}
}

Related

How to customize a table item content on selection and on emphasis change for a NSTableView?

I'm currently trying to create a 3-pane layout in SwiftUI. Since SwiftUI's List doesn't provide greater customisation, I decided to create my own NSViewRepresentableView based on NSTableView. The NSTableView has a single column and each row is represented by a NSHostingView that contains a SwiftUI View. Here's a partial screenshot of how it looks with a single item selected:
I have two questions:
The text should be white color when an item is selected. How do I do this? I have solved this part with tableViewSelectionDidChange() and messing up views, but it has very slow performance and looks like a mess.
When the tableview with some selection loses focus, how do I detect that and go back to black color again?
Here's the code I've written so far:
struct MkList<Data, RowContent> : NSViewRepresentable where Data : RandomAccessCollection, RowContent : View {
var data: Data
var rowContent: (Data.Element) -> RowContent
public init(_ data: Data, #ViewBuilder rowContent: #escaping (Data.Element) -> RowContent) {
self.data = data
self.rowContent = rowContent
}
final class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
var parent: MkList<Data, RowContent>
init(_ parent: MkList<Data, RowContent>) {
self.parent = parent
}
func numberOfRows(in tableView: NSTableView) -> Int {
return parent.data.count
}
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
50
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let x = NSHostingView(rootView: parent.rowContent(parent.data[parent.data.index(parent.data.startIndex, offsetBy: row)], row).foregroundColor(.primary))
return x
}
func tableViewSelectionDidChange(_ notification: Notification) {
let tableView = notification.object as! NSTableView
parent.selection = Set(tableView.selectedRowIndexes.map({ $0 }))
for row in 0..<parent.data.count {
let v: NSHostingView<ModifiedContent<RowContent, _EnvironmentKeyWritingModifier<Color?>>> = tableView.view(atColumn: 0, row: row, makeIfNecessary: true) as! NSHostingView<ModifiedContent<RowContent, _EnvironmentKeyWritingModifier<Color?>>>
v.rootView = parent.rowContent(parent.data[parent.data.index(parent.data.startIndex, offsetBy: row)], row).foregroundColor(parent.selection.contains(row) ? .white : .primary) as! ModifiedContent<RowContent, _EnvironmentKeyWritingModifier<Color?>>
}
}
}
func makeNSView(context: Context) -> NSScrollView {
let tableView = NSTableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.allowsMultipleSelection = true
tableView.headerView = nil
tableView.selectionHighlightStyle = .regular
tableView.gridStyleMask = NSTableView.GridLineStyle.solidHorizontalGridLineMask
let col = NSTableColumn()
col.minWidth = 250
tableView.addTableColumn(col)
let scrollView = NSScrollView()
scrollView.documentView = tableView
scrollView.hasVerticalScroller = true
scrollView.autohidesScrollers = true
return scrollView
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
print("Update table called")
let tableView = (nsView.documentView as! NSTableView)
context.coordinator.parent = self
print(data)
// actually, model should tell us if reload is needed or not
tableView.reloadData()
// ... some basic logic to keep selection state ...
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
}
The MkList View is used as follows:
struct DownloadsView: View {
// The downloads will be retrieved and updated here only
#State var downloadsList: [String] = ["Download 1", "Download 2", "Download 3"]
var body: some View {
MkList(downloadsList) { i in
DownloadListItemView(downloadName: i)
}
}
}
Overall, what is the best approach to solve this problem?

NSOutlineView unexpectedly found nil

I am trying to make a file browser in my app that opens in a side panel (with a split view controller).
The source is a URL brought by a prepareForSegue method in the previous viewController.
Each time the vc loads i have the fatal error :
Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
The compiler locates the error to where i declare :
outlineView.delegate = self
outlineView.dataSource = self
I tried :
1. Undoing and redoing all my outlets connections, by code, by
storyboard
2. Reconnecting delegates and datasource by code, by storyboard
3. I thought maybe something was wrong in my datasource method and i rewrote it 5 times
4. I tried to put my setDelegatesAndDatasource method in the viewDidAppear too, thinking it was a problem of view life cycle
I can't understand what's going on.
Thanks for your help.
'''
extension ViewControllerSource : NSOutlineViewDataSource, NSOutlineViewDelegate {
func setDelegatesAndDatasources(){
outlineView.delegate = self
outlineView.dataSource = self
}
// MARK: - NSOutlineView Datasource
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.children.count
}
return 1
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.children[index]
}
return rootfileSystemItem
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.hasChildren()
}
return false
}
// MARK: - NSOutlineView Delegate
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
guard let colIdentifier = tableColumn?.identifier else { return nil }
if colIdentifier == NSUserInterfaceItemIdentifier(rawValue: "col1") {
let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "cell1")
guard let cell = outlineView.makeView(withIdentifier: cellIdentifier, owner: nil) as? NSTableCellView else { return nil }
if let collection = item as? FileSystemItem {
cell.textField?.stringValue = collection.name ?? "Title not available"
cell.textField?.isEditable = false
cell.textField?.wantsLayer = true
cell.imageView?.image = collection.icon
// cell.textField?.delegate = self
} else {
cell.textField?.stringValue = "unknown item"
cell.textField?.isEditable = false
cell.textField?.wantsLayer = true
}
return cell
} else {
return nil
}
}
}
'''
And here is the main viewController file :
'''
class ViewControllerSource: NSViewController {
#IBOutlet var outlineView: NSOutlineView!
var echo:Echo? {
didSet {
echo!.checkFolderIntegrity()
rootfileSystemItem = FileSystemItem(url: echo!.url)
let window = self.view.window?.windowController as! WindowControllerEcho
window.directoryPath.url = echo!.url
}
}
let propertyKeys: [URLResourceKey] = [.localizedNameKey, .effectiveIconKey, .isDirectoryKey, .typeIdentifierKey]
var rootfileSystemItem: FileSystemItem! {
didSet {
displayItems()
outlineView.reloadData()
}
}
// MARK: - Initialization
override func viewDidLoad() {
super.viewDidLoad()
setDelegatesAndDatasources()
}
func displayItems(){
for fileSystemItem in rootfileSystemItem.children as [FileSystemItem] {
print("item : \(fileSystemItem)")
for subItem in fileSystemItem.children as [FileSystemItem] {
print("\(fileSystemItem.name) - \(subItem.name)")
}
}
}
}
extension ViewControllerSource : EchoDelegate {
func didLoad(echo: Echo) {
self.echo = echo
}
}
'''
The prepareForSegue code reveals the mistake:
You are setting echo in prepareForSegue. This causes to call the property observer didSet. However at this moment the view is not loaded yet and force unwrapping the type crashes.
The solution is to move the code in didSet into viewDidLoad and viewWillAppear and delete the property observer. Nevertheless I recommend to optional bind window
var echo : Echo!
override func viewDidLoad() {
super.viewDidLoad()
setDelegatesAndDatasources()
echo.checkFolderIntegrity()
rootfileSystemItem = FileSystemItem(url: echo.url)
}
override func viewWillAppear(_ animated : Bool) {
super.viewWillAppear(animated)
if let window = self.view.window?.windowController as? WindowControllerEcho {
window.directoryPath.url = echo.url
}
}
Setting delegate and dataSource once is sufficient. If you are using storyboard or Xib the most convenient way is to connect both in Interface Builder.

How to correctly add a row to NSTableView

Firstly I must note that this is my first GUI-app (XIB) in Swift, in other words I am working on and trying to learn Swift and MacOS software development. I have looked through several questions, here at Stack, as well as the Apple documentation on NSTableView, but I'm stuck.
Trying to make a simple app to read some attributes of selected files. I have a custom NSView where the user drags and drop in a file and it reads some attributes off it - which is ok.
>>> print("\(fileDataToShow)\n\(resultTable)")
Optional([["filename": "foo.jpeg", "state": "1"],["filename": "bar.jpeg", "state": "1"]])
Optional(<NSTableView: 0x101203070>)
The #IBOutlet weak var resultTable: NSTableView!, at top of the file containing the class/NSView, show that it is connected, MainMenu.XIB—ResultTable.
I have come up with following code, in an attempt to display the data in the NSTableView, from my custom class View: NSView {
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
<...>
func numberOfRowsInTableView(in tableView: NSTableView) -> Int {
return fileDataToShow?.count ?? 0
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
var result:NSTableCellView
result = tableView.makeView(withIdentifier: (tableColumn?.identifier)!, owner: self) as! NSTableCellView
result.textField?.stringValue = fileDataToShow?[row][(tableColumn?.identifier.rawValue)!]! as! String
return result
}
resultTable?.beginUpdates()
// print(type(of:fileDataToShow)) // Optional<Array<Dictionary<String, Any>>>
resultTable.insertRows(at: IndexSet(integer: fileDataToShow?.count ?? 0), withAnimation: .effectFade)
resultTable.reloadData()
resultTable?.endUpdates()
}
Content of fileDataToShow is ok, but the other lines of code, .beginUpdates() / .insertRows(.., etc. doesn't seem to have any action.
As mentioned, I can't figure this out and don't know where or how to figure this... Anyone got some tips and/or pointers for me ?
I have defined all of the keys in fileDataToShow to correspond with the Identifiers in my XIB.
Hope I have managed to explain my problem in an ok way.
EDIT:
The Debug area giving following output when I run my app:
*** Illegal NSTableView data source (<NSObject: 0x600000000b90>). Must implement numberOfRowsInTableView: and tableView:objectValueForTableColumn:row:
EDIT2/Update:
Thank you #vadian, but I still haven't managed to fix this, here's a little update.
Here's my whole file, DropZone.swift:
```
class DropView: NSView/*, NSTableViewDataSource, NSTableViewDelegate*/ {
#IBOutlet weak var resultTable: NSTableView!
let dropZoneEnteredBackgroundColor = CGColor(red: 165/255, green: 165/255, blue: 165/255, alpha: 1.0)
let dropZoneExitedBackgroundColor = CGColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1.0)//NSColor.gray.cgColor
required init?(coder: NSCoder) {
super.init(coder: coder)
self.wantsLayer = true
self.layer?.backgroundColor = dropZoneExitedBackgroundColor
registerForDraggedTypes([NSPasteboard.PasteboardType.URL,
NSPasteboard.PasteboardType.fileURL])
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
self.layer?.backgroundColor = dropZoneEnteredBackgroundColor
return .copy
}
override func draggingEnded(_ sender: NSDraggingInfo) {
self.layer?.backgroundColor = dropZoneExitedBackgroundColor
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
guard let pasteboard = sender.draggingPasteboard.propertyList(forType:
NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as?
NSArray else {return false}
var droppedItems: [String: String] = [:]
for path in pasteboard {
guard let fullPath = path as? String else { return false }
let fileManager = FileManager.default
var isDir: ObjCBool = false
if fileManager.fileExists(atPath: fullPath, isDirectory:&isDir) {
if isDir.boolValue {
// the dropped item exists and it's a directory
droppedItems[path as! String] = "folder"
}
else {
// file exists and it's not a directory, hence a normal file
droppedItems[path as! String] = "file"
}
}
}
do {
var fileDataToShow = [[String:Any]]()
for object in droppedItems {
if object.value == "file" {
do {
//let fullPath = object.key
let attributes = try object.key.extendedAttributes() // Array<String>
let filename = object.key.fileName() + "." + object.key.fileExtension()
fileDataToShow.append(["state": "1",
"filename": filename,
"metadata":removed_attributes
])
}
catch {
debugPrint("Error info: \(error)")
}
}
else if object.value == "folder" {
// TODO
}
}
func numberOfRows(in tableView: NSTableView) -> Int {
return fileDataToShow.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
let cell = tableView.makeView(withIdentifier: tableColumn?.identifier ?? NSUserInterfaceItemIdentifier(rawValue: ""), owner: self) as! NSTableCellView
// This line could crash if there values which are not String
cell.textField?.stringValue = fileDataToShow[row][tableColumn?.identifier.rawValue ?? ""] as! String
return cell
}
let insertionIndex = fileDataToShow.count
//debugPrint(resultTable) // Optional(<NSTableView: 0x10100ba10>)
//debugPrint(fileDataToShow) // [["filename": "img1.jpeg", "metadata": ["com.apple.metadata..", "com.a..."], "state": "1"]]
resultTable.insertRows(at: IndexSet(integer: insertionIndex), withAnimation: .effectGap)
} // do
return true
}
}
This is now giving the following error:
*** Canceling drag because exception 'NSTableViewException' (reason 'NSTableView error inserting/removing/moving row 2 (numberOfRows: 0).') was raised during a dragging session
Sorry, but have had trouble with this since the last reply from #vadian, so have to ask again.
What am I doing wrong?
EDIT 3:
Appreciate your answers, #vadian, but I do not get this. I have places the numberOfRows and tableView function right underneath the init function. And implemented following code last in the do-block, in an attempt to update the table:
resultTable.beginUpdates()
var i = 0
for row in fileDataToShow {
print("state:",row["state"]!) // 1
print("filename:",row["filename"]!) // file.jpg
print("metadata:",row["metadata"]!) // ["com.apple.metadata..", "com.a..."]
resultTable.insertRows(at: IndexSet(integer: i), withAnimation: .effectFade)
i += 1
}
resultTable.endUpdates()
New lines is added to the table, but they are all empty. How do I - in any way - bind fileDataToShow against resultTable.insertRows.
If you understand my poor spelling and fussy questions :)
Swift is hard but fun to learn!
There are many issues in the code.
numberOfRowsInTableView is numberOfRows(in tableView: in Swift 3+.
The datasource / delegate methods must be on the top level in the class, not in performDragOperation.
You are using too many question marks.
Don not declare the data source array as optional, declare it as empty non-optional array.
var fileDataToShow = [[String:Any]]()
func numberOfRows(in tableView: NSTableView) -> Int {
return fileDataToShow.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
let cell = tableView.makeView(withIdentifier: tableColumn.identifier!, owner: self) as! NSTableCellView
// This line could crash if there values which are not String
cell.textField?.stringValue = fileDataToShow[row][tableColumn.identifier!.rawValue)] as! String
cell result
}
To insert a row with animation don't call reloadData(). Get the last index of the array, append the item to the array and insert the row.
Begin-/endUpdates is useless
let insertionIndex = fileDataToShow.count
fileDataToShow.append([:]) // append some suitable dictionary
resultTable.insertRows(at: IndexSet(integer: insertionIndex), withAnimation: .effectGap)

NSOutlineView, how to get the selected cell

I want to get the selected cell from the NSOutlineView control. I found a solution here: How can I get the selected cell from a NSOutlineView?. It says
Use the delegate. willDisplayCell: is called when a cell changes its selection state.
However when I test it, I found that my willDisplayCell: is not be called.
Here's my code, it can be run normally, but the willDisplayCell: method has never been called. Where did I make a mistake? Thanks.
class TreeNode: NSObject{
var name: String = ""
private(set) var isLeaf: Bool = false
var children: [TreeNode]?
init(name: String, isLeaf: Bool){
self.name = name
self.isLeaf = isLeaf
if !isLeaf{
children = [TreeNode]()
}
}
}
class ViewController: NSViewController {
#IBOutlet weak var sourceList: NSOutlineView!
private var data = TreeNode(name: "Root", isLeaf: false)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
for i in 0..<10{
let node = TreeNode(name: "name \(i)", isLeaf: i % 2 == 0)
data.children?.append(node)
}
}
}
extension ViewController: NSOutlineViewDataSource {
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let item = item as? TreeNode, !item.isLeaf {
return item.children!.count
}
return 1
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
return !((item as? TreeNode)?.isLeaf ?? false)
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let item = item as? TreeNode {
if item.isLeaf{
return item
}else{
return item.children![index]
}
}
return data
}
}
extension ViewController: NSOutlineViewDelegate {
func outlineView(_ outlineView: NSOutlineView, willDisplayCell cell: Any, for tableColumn: NSTableColumn?, item: Any) {
print("called")
}
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
let cell: NSTableCellView?
if let item = item as? TreeNode{
cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DataCell"), owner: self) as? NSTableCellView
cell?.textField?.stringValue = item.name
}else{
cell = nil
}
return cell
}
}
I've solved this problem myself. The following code is how to get the selected cell:
func getSelectedCell() -> NSTableCellView? {
if let view = self.sourceList.rowView(atRow: self.sourceList.selectedRow, makeIfNecessary: false) {
return view.view(atColumn: self.sourceList.selectedColumn) as? NSTableCellView
}
return nil
}
Now I can access the NSTextField control by the code getSelectedCell()?.textField.

EXC_BAD_ACCESS in Simple NSOutlineView DataSource with Structs, but not with Classes

I am making a simple NSOutlineView and am having trouble with crashes and apparent non-deterministic behavior. Here's the code for my view controller.
import Cocoa
struct Hierarchy {
var name: String
var children: [String]
init(name: String, children: [String]) {
self.name = name
self.children = children
}
}
class MainViewController: NSViewController {
#IBOutlet weak var outlineView: NSOutlineView!
var data: [Hierarchy] = [Hierarchy]()
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
self.outlineView.delegate = self
self.outlineView.dataSource = self
self.data = [
Hierarchy(name: "Heading 1", children: ["Abc", "Def", "Ghi"]),
Hierarchy(name: "Heading 2", children: [String]()),
Hierarchy(name: "Heading 3", children: ["Jkl", "Mno", "Pqr"])
]
}
}
extension MainViewController: NSOutlineViewDataSource {
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let heierarchy = item as? Hierarchy {
return heierarchy.children.count
}
return self.data.count
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let heierarchy = item as? Hierarchy {
return heierarchy.children[index]
}
return self.data[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let heierarchy = item as? Hierarchy {
return heierarchy.children.count > 0
}
return false
}
}
extension MainViewController: NSOutlineViewDelegate {
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
var view: NSTableCellView?
if let hierarchy = item as? Hierarchy {
view = outlineView.make(withIdentifier: "AlphaCell", owner: self) as? NSTableCellView
if let textField = view?.textField {
textField.stringValue = hierarchy.name
textField.sizeToFit()
}
} else if let hierarchyChild = item as? String {
view = outlineView.make(withIdentifier: "AlphaCell", owner: self) as? NSTableCellView
if let textField = view?.textField {
textField.stringValue = hierarchyChild
textField.sizeToFit()
}
}
return view
}
}
This will either show nothing in the Outline View, crash when trying to expand one of the headings, or open one of the headings to reveal "Heading 1", "Heading 2", "Heading 3" inside, nested.
HOWEVER. If I change from structs to classes for Hierarchy, everything works. Why is this the case?
class Hierarchy {
var name: String
var children: [String]
init(name: String, children: [String]) {
self.name = name
self.children = children
}
}