initWithPasteboardPropertyList from Objc to swift

I have the following code in a tutorial :


@implementation DesktopEntity
- (id)initWithFileURL:(NSURL *)fileURL {
    self = [super init];
    if (self) {
        _fileURL = fileURL;
    }
    return self;
}
+ (DesktopEntity *)entityForURL:(NSURL *)url {
    NSString *typeIdentifier;
    if ([url getResourceValue:&typeIdentifier forKey:NSURLTypeIdentifierKey error:NULL]) {
        NSArray *imgTypes = [NSImage imageTypes];
        if ([imgTypes containsObject:typeIdentifier]) {
            return [[DesktopImageEntity alloc] initWithFileURL:url];
        } else if ([typeIdentifier isEqualToString:(NSString *)kUTTypeFolder]) {
            return [[DesktopFolderEntity alloc] initWithFileURL:url];
        }
    }
    return nil;
}

- (id)initWithPasteboardPropertyList:(id)propertyList ofType:(NSString *)type {
    NSURL *url = [[NSURL alloc] initWithPasteboardPropertyList:propertyList ofType:type];
    self = [DesktopEntity entityForURL:url];
    return self;
}


, I am not sure how to convert to swift the initWithPasteboardPropertyList

If I call the class function DesktopEntity entityForURL:url, to which object should I assign its result ? Obviously I cannot write


self = DesktopEntity.entityForURL(url!)



here is what I wrote so far


class DesktopEntity: NSObject, NSPasteboardWriting, NSPasteboardReading {
  
    var fileUrl : NSURL
    private var _name : String /
    init(fileURL: NSURL) {
        self.fileUrl = fileURL
        _name = ""
        super.init()
    }
  
    class func entityForURL(url: NSURL) -> DesktopEntity? {
        var typeIdentifier: AnyObject?  /
        do {
            try url.getResourceValue(&typeIdentifier, forKey: NSURLTypeIdentifierKey) /
            let imgTypes = NSImage.imageTypes()
          
            let stringTypeIdentifier = typeIdentifier as! String
            if imgTypes.contains(stringTypeIdentifier) {
                return DesktopImageEntity(fileURL: url)
            } else if stringTypeIdentifier == kUTTypeFolder as String {
                return DesktopFolderEntity(fileURL: url)
            }
        } catch _ {}
      
    return nil /
    }
    var name : String? {
        get {
            var localName: AnyObject?
            do {
                try fileUrl.getResourceValue(&localName, forKey: NSURLLocalizedNameKey)
                    return localName as? String
                } catch _ {  }
            return _name /
        }
        set {
            _name = newValue!
        }
    }

    required init!(pasteboardPropertyList propertyList: AnyObject, ofType type: String) {       
        let url = NSURL(pasteboardPropertyList: propertyList, ofType: type)
                                           // How to implement : DesktopEntity.entityForURL(url!)
        self.fileUrl = url!
        _name = ""
        super.init()
    }

In Swift terms, the behavior of the class method '+ (DesktopEntity *)entityForURL:(NSURL *)url' is basically a convenience initializer that calls "across" to designated initializer 'init(fileURL: NSURL)'.


So I think you can rewrite it as a real convenience init, and then of course you will need to make the 3rd init a convenience init, too.

Thanks, I had misunderstood the mechanism and still miss some points


how can I initialize an instance as a subclass instance ; I understand that's what's done in objc with return DesktopImageEntity(fileURL: url)

if imgTypes.contains(stringTypeIdentifier) {
     return DesktopImageEntity(fileURL: url)
@interface DesktopImageEntity : DesktopEntity
@implementation DesktopImageEntity
- (NSImage *)image {
    if (!_image) {
        _image = [[NSImage alloc] initByReferencingURL:self.fileURL];
    }
    return _image;
}
@end


I would like to do something like


self.init(fileURL: url) as DesktopImageEntity


in my convenience init?


    convenience init?(url: NSURL) {
        var typeIdentifier: AnyObject?
        do {
            try url.getResourceValue(&typeIdentifier, forKey: NSURLTypeIdentifierKey) /
            let imgTypes = NSImage.imageTypes()
           
            let stringTypeIdentifier = typeIdentifier as! String
            if imgTypes.contains(stringTypeIdentifier) {
                self.init(fileURL: url)
            } else if stringTypeIdentifier == kUTTypeFolder as String {
                self.init(fileURL: url
            }
        } catch _ {}
       
    return nil /
    }

To be more p^recise, my problem is to find how to implement the equivalent of self = [DesktopEntity entityForURL:url];:


- (id)initWithPasteboardPropertyList:(id)propertyList ofType:(NSString *)type {
    NSURL *url = [[NSURL alloc] initWithPasteboardPropertyList:propertyList ofType:type];
    self = [DesktopEntity entityForURL:url];
    return self;
}


I need to do it inside an initializer :


    required init?(pasteboardPropertyList propertyList: AnyObject, ofType type: String) {
        let url = NSURL(pasteboardPropertyList: propertyList, ofType: type)
        self.fileUrl = url!
        _name = ""
        super.init()
    }

I haven't tested this, but I think it would be like this:

    required init?(pasteboardPropertyList propertyList: AnyObject, ofType type: String) {
        let url = NSURL(pasteboardPropertyList: propertyList, ofType: type)
        self.init(url: url)
        _name = ""

    }

You want to call another DesktopEntity initilizer (self.init(...)), not an NSObject initializer (super.init()).

You'll need to change that initializer to a class method:


class func entityWithPasteboardPropertyList (propertyList: AnyObject, ofType type: String) -> DesktopEntity? {
     let url = NSURL(pasteboardPropertyList: propertyList, ofType: type)!
     let entity = entityForURL (url)
     entity.fileUrl = url
     entity._name = ""
     return entity

    }

I get a few compiler's erros (asks for convenience), so it works with :


   required convenience init?(pasteboardPropertyList propertyList: AnyObject, ofType type: String) {
        let url = NSURL(pasteboardPropertyList: propertyList, ofType: type)
        self.init(fileURL: url!)
        _name = ""
     }
     



But, that deoies not allow to call for the equivalent of

self = [DesktopEntity entityForURL:url];


What I would need ultimately, is the init to create an instance which is a subtype of the class where this init is ...

Well, but this initializer irequired to conform to protocol 'NSPasteboardReading'...


I fear it's the whole architecture of my code that needs redesign !

For reference, the original objc code is from CocoaProgramming tutorials (lesson 56) on github


lucasderraugh/AppleProg-Cocoa-Tutorials

Oops, sorry, I didn't review the OP and your other posts carefully enough. I didn't catch that the original code was calling a class function instead of another initializer.

I don't see what you need the factory method for.

How about this:


import AppKit

class DesktopEntity: NSObject, /NSPasteboardWriting, */NSPasteboardReading
   {
    var fileUrl:NSURL
    private var _name :String

    init( fileURL:NSURL )
      {
        self.fileUrl = fileURL
        _name = ""
        super.init()
      }

  required convenience init?( pasteboardPropertyList propertyList:AnyObject, ofType type:String )
   {
   if let url = NSURL( pasteboardPropertyList:propertyList, ofType:type )
    {
    self.init(fileURL:url)
    }
   else {return nil}
   }

  static func readableTypesForPasteboard( pasteboard:NSPasteboard )->[String]
   {
   return [kUTTypeURL as String]
   }
  }

class DesktopImageEntity:DesktopEntity
  {
  var _image:NSImage?

  var image:NSImage? {get
    {
    if (_image == nil ) { _image = NSImage( byReferencingURL:fileUrl ) }
    return _image;
    }}

  convenience init?( fromPossiblyNonImageFile file:NSURL )
   {
   var typeIdentifier:AnyObject?
   do {
      try file.getResourceValue( &typeIdentifier, forKey:NSURLTypeIdentifierKey )
      let imgTypes = NSImage.imageTypes()

      let stringTypeIdentifier = typeIdentifier as! String  
      if imgTypes.contains(stringTypeIdentifier) { self.init(fileURL:file) }
      else {return nil}
      }
    catch {return nil}
    }
  }

let fileURL = NSURL( fileURLWithPath:"path to image file" )
let desktopImageEntity1 = DesktopImageEntity( fileURL:fileURL ) /
let desktopImageEntity2 = DesktopImageEntity( fromPossiblyNonImageFile:fileURL ) // contains an image or is nil


BTW shouldn't NSImage( byReferencingURL:NSURL ) be failable?

>> Well, but this initializer irequired to conform to protocol 'NSPasteboardReading'...

In that case you need both: a factory method (to get the object to be of the correct class) and an initializer to satisfy the protocol. The initializer part is essentially trivial, except that you have to have all the classes implement it, and that means you negotiate the init inheritance rules, and what's inherited depends on what other init each class implements. So, you might have to add some boilerplate inits that simply call through to super. It's tedious to satisfy the compiler, but not inherently complicated.


I fear it's the whole architecture of my code that needs redesign !


That's another approach. 🙂 The style of this code is very Obj-C ( old-fashioned Obj-C at that), and a re-imagining in Swift terms would be excellent.

Yes, it is objc as an initial code !

In the objc code, entityForURL:url is used in initWithPasteboardPropertyList to create an instance of a given type (Dektop or DesktopImage) ;

- (id)initWithPasteboardPropertyList:(id)propertyList ofType:(NSString *)type {
    NSURL *url = [[NSURL alloc] initWithPasteboardPropertyList:propertyList ofType:type];
    self = [DesktopEntity entityForURL:url];
    return self;
}

this is necessary for tableView:(NSTableView *)tableView updateDraggingItemsForDrag:(id<NSDraggingInfo>)draggingInfo

to filter the objects according to their class or sunclass in enumerateDraggingItemsWithOptions

So, the logic in objc is :

- enumerateDraggingItemsWithOptions in tableView:(NSTableView *)tableView updateDraggingItemsForDrag

calls initWithPasteboardPropertyList which

calls entityForURL, which create and returns an instance depending on url type:

DesktopImage

DesktopFolder

this is the draggingItem ; and it makes it possible to test whether draggingItem is DesktopImage or not.

Here is the part of objc code that does that.

- (void)tableView:(NSTableView *)tableView updateDraggingItemsForDrag:(id<NSDraggingInfo>)draggingInfo {
    if ([draggingInfo draggingSource] != tableView) {
        NSArray *classes = @[[DesktopEntity class], [NSPasteboardItem class]];
        NSTableCellView *tableCellView = [tableView makeViewWithIdentifier:@"ImageCell" owner:self];
        __block NSInteger validCount = 0;
        [draggingInfo enumerateDraggingItemsWithOptions:0 forView:tableView classes:classes searchOptions:nil usingBlock:^(NSDraggingItem *draggingItem, NSInteger idx, BOOL *stop) {
            if ([draggingItem.item isKindOfClass:[DesktopEntity class]]) {
                DesktopEntity *entity = (DesktopEntity *)draggingItem.item;
                draggingItem.draggingFrame = [tableCellView frame];
                draggingItem.imageComponentsProvider = ^NSArray *{
                    if ([entity isKindOfClass:[DesktopImageEntity class]])
                        [tableCellView.imageView setImage:[(DesktopImageEntity *)entity image]];
                    [tableCellView.textField setStringValue:entity.name];
........
}

I do not see how it will work in Swift; If I understand how all this works:

- enumerateDraggingItemsWithOptions in tableView:(NSTableView *)tableView updateDraggingItemsForDrag

calls convenience init?( pasteboardPropertyList

which can only create a Desktop class object (not DesktopImage)

I must be missing a step !

In fact, I would need to call init for the subclass, to return an entity of the subclass of DesktopEntity after init, such as :


DesktopImageEntity.init()


but that doesn't work

Thanks for help, I'm getting closer to it.


Remeber the context :

- Doing drag and drop from objects in Finder to a tableView

- I have a DesktopEntity class and sub classes DesktopImageEntity (adding an image property) and DesktopFolderEntity

The problem is how could pasteboardPropertyList return the right information :

1 - count only objects that can be dropped on tableView

2 - include image when required to the object created


I changed the required init as follows and 1. is working:


    required init?(pasteboardPropertyList propertyList: AnyObject, ofType type: String) {
        let url = NSURL(pasteboardPropertyList: propertyList, ofType: type)
        self.fileUrl = url!
        _name = ""
        var typeIdentifier: AnyObject?  /
        do {
            try url!.getResourceValue(&typeIdentifier, forKey: NSURLTypeIdentifierKey) /
            let stringTypeIdentifier = typeIdentifier as! String
            let imgTypes = NSImage.imageTypes()
            if imgTypes.contains(stringTypeIdentifier) {
                super.init()
                if self.isKindOfClass(DesktopImageEntity) { /
                    (self as? DesktopImageEntity)!.image = NSImage(byReferencingURL: url!)
                }
                return
            } else if stringTypeIdentifier == kUTTypeFolder as String {
                super.init()
                return
            }
            super.init()
            return nil
        } catch _ {
            super.init()
            return nil
        } /
    }


I initially wrote :


if imgTypes.contains(stringTypeIdentifier) {
                super.init()
                 (self as? DesktopImageEntity)!.image = NSImage(byReferencingURL: url!)


Compiler accepted it but it fails at execution :

Could not cast value of type 'DesktopEntity' (0x100016b90) to 'DesktopImageEntity' (0x100016c90).

I added the isKindOf test, but of course, it always fails.


So 2 questions :

- why the error with the conditional downcast ?

- how to make the downcast effective so that I can define the image ?

Accepted Answer

I think you are making this far too complicated.

Can't you just add an optional image field to your DeskTopItem class and forget about the DeskTopImageItem class?

Then you can simply do


if let image = deskTopItem.image {tableCellView.imageView.setImage(image)}

initWithPasteboardPropertyList from Objc to swift
 
 
Q