1

I'm making my first attempts with Swift's async/await features and Task.

My first goal is working with NSItemProvider. I have a custom class that extends NSObject and conforms to NSSecureCoding. I have working code that converts my class to and from NSItemProvider.

What I'm now trying to do is convert an array of NSItemProvider objects back into an array of MyClass objects. Each conversion requires a call to an async function.

I'm able to do this using a plain for in loop. The trouble begins when I try to use compactMap or forEach to create the new array from the array of NSItemProvider.

Here is some code that can be copied into a Swift Playground or project that demonstrates the issue.

import Foundation

class MyClass: NSObject, NSSecureCoding {
    var name: String

    override var description: String {
        return name
    }

    init(name: String) {
        self.name = name
    }

    static var supportsSecureCoding = true

    func encode(with coder: NSCoder) {
        coder.encode(name, forKey: "name")
    }

    required init?(coder: NSCoder) {
        if let name = coder.decodeObject(of: NSString.self, forKey: "name") as? String {
            self.name = name
        } else {
            return nil
        }
    }
}

extension MyClass {
    static let identifier = "mySpecialId"

    func asItemProvider() -> NSItemProvider {
        return NSItemProvider(item: self, typeIdentifier: Self.identifier)
    }

    static func create(withItemProvider itemProvider: NSItemProvider) async throws -> Self? {
        let res = try await itemProvider.loadItem(forTypeIdentifier: identifier)
        return res as? Self
    }
}

func example() {
    Task {
        let objs = [ MyClass(name: "A"), MyClass(name: "B"), MyClass(name: "C"), MyClass(name: "D") ]
        let providers: [NSItemProvider] = objs.map { $0.asItemProvider() }

        do {
            // This basic `for in` loop works
            var newObjs = [MyClass]()
            for provider in providers {
                if let obj = try await MyClass.create(withItemProvider: provider) {
                    newObjs.append(obj)
                }
            }
            print("newObjs: \(newObjs)")

            // Attempt to use `forEach`
            // Error: Cannot pass function of type '(NSItemProvider) async -> MyClass?' to parameter expecting synchronous function type
            /*
            providers.forEach { provider in
                if let obj = try await MyClass.create(withItemProvider: provider) {
                    newObjs.append(obj)
                }
            }
             */

            // Attempt to use `compactMap`
            // Error: Cannot pass function of type '(NSItemProvider) async -> MyClass?' to parameter expecting synchronous function type
            //let newObjs2 = providers.compactMap { await MyClass.create(withItemProvider: $0) }
        } catch {

        }
    }
}

I was looking at another question but the solutions are far more complicated than just using a simple for in loop.

Is there a simpler way to use compactMap, for example, to directly convert the array of NSItemProvider into an array of MyClass?

HangarRash
  • 7,314
  • 5
  • 5
  • 32
  • There is no simple way if asynchronous data processing is involved. The async/await way is `TaskGroup`. By the way have a look at the new [`Transferable`](https://developer.apple.com/documentation/CoreTransferable/Transferable?changes=_3) protocol which is less *objective-c-ish* than `NSItemProvider`. – vadian Nov 05 '22 at 21:19
  • @vadian I'll have to look into TaskGroup more. But what I've seen it's a lot more verbose than the `for in` loop I posted in this question. I'll also have a look into Transferable. I'm using `NSItemProvider` for both drag and drop (in collection views, table views, and custom views) as well as with the pasteboard. Thanks. – HangarRash Nov 05 '22 at 21:28

1 Answers1

0

for in loop is a lower-level feature built into Swift.

forEach (as well as compactMap) are methods implemented on the Array type. Being a function it expects arguments of a certain type - in the forEach case this is (Self.Element) throws -> Void

When you call

providers.forEach { provider in
    if let obj = try await MyClass.create(withItemProvider: provider) {
         newObjs.append(obj)
    }
}

You are passing in a closure of type (NSItemProvider) async throws -> Void which is obviously not what the compiler expects and provides a (very informative I must say) error message.

Hopefully soon we will have these functions working 'natively' with async-await. Meanwhile Sundell might have an idea how to do it if you insist on using this syntax which could potentially be useless anyway.

Petar
  • 2,241
  • 1
  • 24
  • 38