Swift code to use NSOutlineView as file system dir

2020-06-27 12:17发布

问题:

I'm struggling with this Swift code already for some time and do not find the problem. The code below should provide the File Directory as DataSource for a NSOutlineView. The GUI is quite simple just a window with a NSOutlineView and a Object for the OutlineViewController instance. When I start the application it shows the root entry, when I expand the root entry it shows for a short period the sub items. Then the application crashes with an Error in file "main.swift" at line "NSApplicationMain(C_ARGC, C_ARGV) --> "EXC_BAD_ACCESS(code=EXC_I386_GPFLT)" ?

If added some println() to proof the directory structure - this seems to be fine.

The swift code:

import Cocoa
import Foundation

class FileSystemItem {

    let propertyKeys = [NSURLLocalizedNameKey, NSURLEffectiveIconKey, NSURLIsPackageKey, NSURLIsDirectoryKey,NSURLTypeIdentifierKey]
    let fileURL: NSURL

    var name: String! {
    let resourceValues = fileURL.resourceValuesForKeys([NSURLNameKey], error: nil)
        return resourceValues[NSURLNameKey] as? NSString
    }

    var localizedName: String! {
    let resourceValues = fileURL.resourceValuesForKeys([NSURLLocalizedNameKey], error: nil)
        return resourceValues[NSURLLocalizedNameKey] as? NSString
    }

    var icon: NSImage! {
    let resourceValues = fileURL.resourceValuesForKeys([NSURLEffectiveIconKey], error: nil)
        return resourceValues[NSURLEffectiveIconKey] as? NSImage
    }

    var dateOfCreation: NSDate! {
    let resourceValues = self.fileURL.resourceValuesForKeys([NSURLCreationDateKey], error: nil)
        return resourceValues[NSURLCreationDateKey] as? NSDate
    }

    var dateOfLastModification: NSDate! {
    let resourceValues = fileURL.resourceValuesForKeys([NSURLContentModificationDateKey], error: nil)
        return resourceValues[NSURLContentModificationDateKey] as? NSDate
    }

    var typeIdentifier: String! {
    let resourceValues = fileURL.resourceValuesForKeys([NSURLTypeIdentifierKey], error: nil)
        return resourceValues[NSURLTypeIdentifierKey] as? NSString
    }

    var isDirectory: String! {
    let resourceValues = fileURL.resourceValuesForKeys([NSURLIsDirectoryKey], error: nil)
        return resourceValues[NSURLIsDirectoryKey] as? NSString
    }

    var children: [FileSystemItem] {

        var childs: [FileSystemItem] = []
        var isDirectory: ObjCBool = ObjCBool(1)
        let fileManager = NSFileManager.defaultManager()
        var checkValidation = NSFileManager.defaultManager()

        if (checkValidation.fileExistsAtPath(fileURL.relativePath)) {

            if let itemURLs = fileManager.contentsOfDirectoryAtURL(fileURL, includingPropertiesForKeys:propertyKeys, options:.SkipsHiddenFiles, error:nil) {

                for fsItemURL in itemURLs as [NSURL] {

                    if (fileManager.fileExistsAtPath(fsItemURL.relativePath, isDirectory: &isDirectory))
                    {
                        if(isDirectory == true) {
                            let checkItem = FileSystemItem(fileURL: fsItemURL)
                            childs.append(checkItem)
                        }
                    }

                }
            }
        }
        return childs
    }

    init (fileURL: NSURL) {
        self.fileURL = fileURL
    }

    func hasChildren() -> Bool {
        return self.children.count > 0
    }

}


class OutlineViewController : NSObject, NSOutlineViewDataSource {

    let rootFolder : String = "/"
    let rootfsItem : FileSystemItem
    let fsItemURL : NSURL
    let propertyKeys = [NSURLLocalizedNameKey, NSURLEffectiveIconKey, NSURLIsPackageKey, NSURLIsDirectoryKey,NSURLTypeIdentifierKey]

    init() {

        self.fsItemURL = NSURL.fileURLWithPath(rootFolder)
        self.rootfsItem = FileSystemItem(fileURL: fsItemURL)

        for fsItem in rootfsItem.children as [FileSystemItem] {
            for fsSubItem in fsItem.children as [FileSystemItem] {
                println("\(fsItem.name) - \(fsSubItem.name)")
            }

        }
    }

    func outlineView(outlineView: NSOutlineView!, numberOfChildrenOfItem item: AnyObject!) -> Int {
        if let theItem: AnyObject = item {
            let tmpfsItem: FileSystemItem = item as FileSystemItem
            return tmpfsItem.children.count
        }
        return 1
    }

    func outlineView(outlineView: NSOutlineView!, isItemExpandable item: AnyObject!) -> Bool {
        if let theItem: AnyObject = item {
            let tmpfsItem: FileSystemItem = item as FileSystemItem
            return tmpfsItem.hasChildren()
        }
        return false
    }

    func outlineView(outlineView: NSOutlineView!, child index: Int, ofItem item: AnyObject!) -> AnyObject! {
        if let theItem: AnyObject = item {
            let tmpfsItem: FileSystemItem = item as FileSystemItem
            return tmpfsItem.children[index]
        }
        return rootfsItem
    }

    func outlineView(outlineView: NSOutlineView!, objectValueForTableColumn tableColumn: NSTableColumn!, byItem item: AnyObject!) -> AnyObject! {
        if let theItem: AnyObject = item {
            let tmpfsItem: FileSystemItem = item as FileSystemItem
            return tmpfsItem.localizedName
        }
        return "-empty-"
    }
}



class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet var window: NSWindow

    func applicationDidFinishLaunching(aNotification: NSNotification?) {
        // Insert code here to initialize your application

    }

    func applicationWillTerminate(aNotification: NSNotification?) {
        // Insert code here to tear down your application

    }
}

Any hints ?

回答1:

I had a similar problem with EXC_BAD_ACCESS on an NSOutlineView - with an NSOutlineViewDataSource. The same behaviour of as soon as the node was expanded, the data was displayed then the crash occurred. Some profiling in instruments showed that somewhere a Zombie object was created, and then the Outline view tried to access it.

I think this is a bug - but I managed to get around it by changing all Swift 'Strings' to 'NSStrings'. This may have to be done for all Swift types if you are using them.

In order to ensure everything was an NSString, I had to declare constants within the class such as:

var empty_string : NSString = ""

Because anytime I fed it a Swift string all hell broke loose. Oh well hopefully this will be fixed in the future!



回答2:

So, just to clarify what is going on. NSOutlineView does not retain objects that it is given for its "model"; it was always expected that the client would retain them. For ARC code, this doesn't work well, because if you return a new instance to the NSOutlineView methods the object will not be retained by anything and will quickly be freed. Then subsequent outlineView delegate methods the touch these objects will lead to crashes. The solution to that is to retain the objects yourself in your own array.

Note that the objects returned from objectValueForTableColumn are retained by the NSControl's objectValue.

Back to Swift: As Thomas noted the objects have to be objc objects since they are bridged to an objc class. A Swift string is implicitly bridged to a temporary NSString. This leads to a crash because of the above issue, since nothing retains the NSString instance. That is why maintaining an array of NSStrings "solves" this problem.

The solution would be for NSOutlineView to have an option to retain the items given to it. Please consider logging a bug request for it to do this through bugreporter.apple.com

Thanks, corbin (I work on NSOutlineView)



回答3:

It seems that

outlineView(outlineView: NSOutlineView!, objectValueForTableColumn tableColumn: NSTableColumn!, byItem item: AnyObject!) -> AnyObject!

needs to return an object that conforms to obj-c protocol. So you can return

@objc class MyClass {
  ...
}

(or NSString and the like). But not native Swift stuff like String or Array etc.



回答4:

I believe one of the problems going on here is the fact that the "children" array is getting replaced every time the children property is accessed.

I think this causes some weak references inside the NSOutlineView to break when it queries the DataSource for information.

If you cache the "children" and access the cache to compute "numberOfChildren" and "getChildForIndex" you should see an improvement.



回答5:

In Swift 3.0 I used the following code, which compiles and runs without problems. It is far away from being complete but a step in the right direction, since I am trying to translate TreeTest into Swift.

import Cocoa
import Foundation

class FileSystemItem: NSObject {

let propertyKeys: [URLResourceKey] = [.localizedNameKey, .effectiveIconKey, .isDirectoryKey, .typeIdentifierKey]
var fileURL: URL

var name: String! {
    let resourceValues = try! fileURL.resourceValues(forKeys: [.nameKey])
    return resourceValues.name
}

var localizedName: String! {
    let resourceValues = try! fileURL.resourceValues(forKeys: [.localizedNameKey])
    return resourceValues.localizedName
}

var icon: NSImage! {
    let resourceValues = try! fileURL.resourceValues(forKeys: [.effectiveIconKey])
    return resourceValues.effectiveIcon as? NSImage
}

var dateOfCreation: Date! {
    let resourceValues = try! fileURL.resourceValues(forKeys: [.creationDateKey])
    return resourceValues.creationDate
}

var dateOfLastModification: Date! {
    let resourceValues = try! fileURL.resourceValues(forKeys: [.contentModificationDateKey])
    return resourceValues.contentAccessDate
}

var typeIdentifier: String! {
    let resourceValues = try! fileURL.resourceValues(forKeys: [.typeIdentifierKey])
    return resourceValues.typeIdentifier
}

var isDirectory: Bool! {
    let resourceValues = try! fileURL.resourceValues(forKeys: [.isDirectoryKey])
    return resourceValues.isDirectory
}

init(url: Foundation.URL) {
    self.fileURL = url
}

var children: [FileSystemItem] {
    var childs: [FileSystemItem] = []
    let fileManager = FileManager.default
    // show no hidden Files (if you want this, comment out next line)
    // let options = FileManager.DirectoryEnumerationOptions.skipsHiddenFiles
    var directoryURL = ObjCBool(false)
    let validURL = fileManager.fileExists(atPath: fileURL.relativePath, isDirectory: &directoryURL)
    if (validURL && directoryURL.boolValue) {
        // contents of directory
        do {
            let childURLs = try
                fileManager.contentsOfDirectory(at: fileURL, includingPropertiesForKeys: propertyKeys, options: [])
            for childURL in childURLs {
                let child = FileSystemItem(url: childURL)
                childs.append(child)
            }
        }
        catch {
            print("Unexpected error occured: \(error).")
        }
    }
    return childs
}

func hasChildren() -> Bool {
    return self.children.count > 0
}
}

class OutLineViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource {

@IBOutlet weak var outlineView: NSOutlineView!
@IBOutlet weak var pathController: NSPathControl!

var fileSystemItemURL: URL!
let propertyKeys: [URLResourceKey] = [.localizedNameKey, .effectiveIconKey, .isDirectoryKey, .typeIdentifierKey]
var rootfileSystemItem: FileSystemItem!
var rootURL: URL!

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    let userDirectoryURL = URL(fileURLWithPath: NSHomeDirectory())
    // directory "Pictures" is set as root
    let rootURL = userDirectoryURL.appendingPathComponent("Pictures", isDirectory: true)
    self.pathController.url = rootURL
    self.rootfileSystemItem = FileSystemItem(url: rootURL)

    for fileSystemItem in rootfileSystemItem.children as [FileSystemItem] {
        for subItem in fileSystemItem.children as [FileSystemItem] {
            print("\(fileSystemItem.name) - \(subItem.name)")
        }
    }
//FileSystemItem.rootItemWithPath(self.pathControl.URL.path)
//self.searchForFilesInDirectory(picturesPath)
}

override var representedObject: Any? {
    didSet {
    // Update the view, if already loaded.
    }
}

@IBAction func pathControllerAction(_ sender: NSPathControl) {
    print("controller clicked")
}

// MARK: - outline data source methods

func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
    if let fileSystemItem = item as? FileSystemItem {
        return fileSystemItem.children.count
    }
    return 1
}

func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
    if let fileSystemItem = item as? FileSystemItem {
        return fileSystemItem.hasChildren()
    }
    return false
}

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, objectValueFor tableColumn: NSTableColumn?, byItem item: Any?) -> Any? {
    if let fileSystemItem = item as? FileSystemItem {
        switch tableColumn?.identifier {
        case "tree"?:
            return fileSystemItem.localizedName
        case "coordinate"?:
            return " empty "
        default:
            break
        }
    }
    return " -empty- "
}

// MARK: - outline view delegate methods

func outlineView(_ outlineView: NSOutlineView, shouldEdit tableColumn: NSTableColumn?, item: Any) -> Bool {
    return false
}
}

With a new edit the outline view now shows all files and directories. You can influence the appearance in the children section in class FileSystemItem.