[SwiftUI] SwiftUI Drag&Drop implementation(Vol. 3:use own UTI in Drag&Drop)

SwiftUI

Last 2 posts, implemented drag & drop for String type and my own class with using public.utf8-plain-text. In this post, let's implement drag and drop with using my own UTI.

Environment

This post is checked with following environment

  • macOS Big Sur 11.2.3
  • Xcode 12.4
SwiftUI[SwiftUI] SwiftUI で実装する Drag&Drop (その1: String を Drag&Drop) SwiftUI[SwiftUI] SwiftUI で実装する Drag&Drop (その2:クラスオブジェクト を Drag&Drop)

define own UTI type

First let's define own UTI type with using extension.

example

extension UTType {
    static var myOwnType: UTType {
        UTType(exportedAs: "com.mycompany.myowntype")
    }
}

Usually reverese DNS is recommended for new UTIs.

There are two options for defining UTI. one is "ExportedAs", another is "ImportedAs".
For your own UTI, need to use "ExportedAs". this means you are the owner of the UTI.

"ImportedAs" will be used for "public.text" for example. that means another company/person owns the UTI, but your app can handle the type.

for detail, please refer to the apple doc here.

Define UTI type again (in Info.plist)

Because OS also needs to understand the UTIs which will be handled by application.

You need to put UTI information in Info.plist.

Conveniently Xcode provide the GUI for maintaining the UTI.

UTI definition in Xcode
UTI definition in Xcode
  1. Click target (for the application)
  2. Click Info
  3. define identifier (com.mycompany.myowntype in this case)
  4. need to conforms to public.item (or subclass of it) for Drag and drop (public.item in this case)

Followings are XML expression, so you can edit Info.plist manually.

コード
	UTExportedTypeDeclarations
	
		
			UTTypeConformsTo
			
				public.item
			
			UTTypeIcons
			
			UTTypeIdentifier
			com.mycompany.myowntype
			UTTypeTagSpecification
			
		
	

Note1: Xcode will check the definition at compile time

In case you define your own UTI only in code, Xcode will give the warning.

Note2: UTI needs to conform physical hierarchy for Drag&Drop

If want to use in Drag and Drop, new UTI needs to conform to one of the physical hierarchy type (like public.item).

Note3: conformation information should be defined in Info.plist

Above conformance information should be defined in Info.plist (for Drag&Drop). Otherwise does not work.

Use own UTI type

Just use it as one of UTType.

Use new own UTI type in MyDropData class

Here is the code.

MyDropData

extension UTType {
    static var myOwnType: UTType {
        UTType(exportedAs: "com.mycompany.myowntype")
    }
}

final class MyDropData: NSObject, NSItemProviderReading, NSItemProviderWriting {
    var str: String
    init(_ str:String) {
        self.str = str
    }
    
    static var readableTypeIdentifiersForItemProvider: [String] { [UTType.myOwnType.identifier] } 
    static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> MyDropData {
        guard typeIdentifier == UTType.myOwnType.identifier else { throw MyError.unknownType }
        if let str = String(data: data, encoding: .utf8) {
            return MyDropData(str)
        }
        throw MyError.generateError
    }

    static var writableTypeIdentifiersForItemProvider: [String] { [UTType.myOwnType.identifier] }
    func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
        let data = str.data(using: .utf8)
        completionHandler(data, nil)
        return nil
    }
}

just use own UTI instead of kUTTypeUTF8PlainText.

Note: In some cases, may need to use own type as String. we can use identifier static method of UTType for such cases.

Use new own UTI in SwiftUI

As you see, basically we don't need to change the code. because MyDropData class handles type information as well.

ContentView

struct ContentView: View {
    @State private var dragText2 = "Drag from"
    @State private var dropText2 = "Drop to"

    @State private var isTarget2 = false
    var body: some View {
        HStack {
            List {
                Text(dragText2)
                    .onDrag  {
                        return NSItemProvider(object: MyDropData(dragText2))
                    }
            }
            List {
                Text(dropText2)
                    .onDrop(of: MyDropData.readableTypeIdentifiersForItemProvider, isTargeted: $isTarget2) { providers -> Bool in
                        guard let provider = providers.first else { return false }
                        guard provider.canLoadObject(ofClass: MyDropData.self) else { return false }
                        provider.loadObject(ofClass: MyDropData.self) { (data, error) in
                            if let myDropData = data as? MyDropData {
                                dropText2 += " \(myDropData.str)"
                            }
                        }
                        return true
                    }
            }
        }
    }
}

code

whole code

import SwiftUI
import UniformTypeIdentifiers

enum MyError: Error {
    case unknownType
    case generateError
}

extension UTType {
    static var myOwnType: UTType {
        UTType(exportedAs: "com.mycompany.myowntype")
    }
}

final class MyDropData: NSObject, NSItemProviderReading, NSItemProviderWriting {
    var str: String
    init(_ str:String) {
        self.str = str
    }
    
    static var readableTypeIdentifiersForItemProvider: [String] { [UTType.myOwnType.identifier] }
    static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> MyDropData {
        guard typeIdentifier == UTType.myOwnType.identifier else { throw MyError.unknownType }
        if let str = String(data: data, encoding: .utf8) {
            return MyDropData(str)
        }
        throw MyError.generateError
    }

    static var writableTypeIdentifiersForItemProvider: [String] { [UTType.myOwnType.identifier] }
    func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
        let data = str.data(using: .utf8)
        completionHandler(data, nil)
        return nil
    }
}

struct ContentView: View {
    @State private var dragText2 = "Drag from"
    @State private var dropText2 = "Drop to"

    @State private var isTarget2 = false
    var body: some View {
        HStack {
            List {
                Text(dragText2)
                    .onDrag  {
                        return NSItemProvider(object: MyDropData(dragText2))
                    }
            }
            List {
                Text(dropText2)
                    .onDrop(of: MyDropData.readableTypeIdentifiersForItemProvider, isTargeted: $isTarget2) { providers -> Bool in
                        guard let provider = providers.first else { return false }
                        guard provider.canLoadObject(ofClass: MyDropData.self) else { return false }
                        provider.loadObject(ofClass: MyDropData.self) { (data, error) in
                            if let myDropData = data as? MyDropData {
                                dropText2 += " \(myDropData.str)"
                            }
                        }
                        return true
                    }
            }
        }
    }
}

Summary:how to handle my own UTI in Drag&Drop

how to handle my own UTI in Drag&Drop
  • need to define UTI in code. use "ExportedAs" in case you're the owner of the UTI. if not, use "ImportedAs" instead.
  • need to define UTI in Info.plist too.
  • for using in Drag&Drop, the UTI needs to conform one of the physical type (i.e. descendants of public.item)

Thank you for reading. Hope you enjoy this post.
please feel free to contact to twitter.

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です