Swift-winui / uwp - Issue with FileOpenPicker?

I have success playing with many WinUI controls in Swift but scratching my head on how to work with Swift UWP fonctions like pickSingleFileAsync in the FileOpenPicker class.

The function is not marked async so we cannot await.

Following a simple code example.
Clicking on the displayed button the FileOpenPicker dialog is not displayed and the following error is printed in the windows terminal:

getResults error: A method was called at an unexpected time.

The result type is AnyIAsyncOperation
How deal with such AnyIAsyncOperation?

import Foundation
import UWP
import WinAppSDK
import WindowsFoundation
import WinUI

@main
public class PreviewApp: SwiftApplication {
    override public func onLaunched(_ args: WinUI.LaunchActivatedEventArgs) {
        let window = Window()
        window.title = "Test FileOpenPicker"

        try! window.activate()

        let button = Button()
        button.content = "Open File"
        button.click.addHandler { _, _ in
            let fileOpenPicker: FileOpenPicker = FileOpenPicker()
            fileOpenPicker.fileTypeFilter.append("*")
            fileOpenPicker.viewMode = .thumbnail
            fileOpenPicker.suggestedStartLocation = .desktop
            do {
                let result = try fileOpenPicker.pickSingleFileAsync()

                // ???????????????????
                // result type is AnyIAsyncOperation<StorageFile?>?
                // how wait for this async operation to complete and handle the result?
                // ???????????????????

                if let result = result {
                    do {
                        if let results = try result.getResults() {
                            print("filename: \(results.name)")
                        }
                    }
                    catch {print("getResults error: \(error)")}
                }    
            }
            catch {print("pickOpenFileAsync error: \(error)")}
        }

        let panel = StackPanel()
        panel.orientation = .vertical
        panel.spacing = 10
        panel.horizontalAlignment = .center
        panel.verticalAlignment = .center
        panel.children.append(button)
        window.content = panel
    }
}

If you're using the swift-winui library from The Browser Company, I found the following definition.

/// [Open Microsoft documentation](https://learn.microsoft.com/uwp/api/windows.foundation.iasyncoperation-1)
public protocol IAsyncOperation<TResult> : IAsyncInfo {
    associatedtype TResult
    /// [Open Microsoft documentation](https://learn.microsoft.com/uwp/api/windows.foundation.iasyncoperation-1.getresults)
    func getResults() throws -> TResult
    /// [Open Microsoft documentation](https://learn.microsoft.com/uwp/api/windows.foundation.iasyncoperation-1.completed)
    var completed: WindowsFoundation.AsyncOperationCompletedHandler<TResult>? { get set }
}

public typealias AnyIAsyncOperation<TResult> = any IAsyncOperation<TResult>

public extension IAsyncOperation {
    func get() async throws -> TResult {
        if status == .started {
            let event = WaitableEvent()
            completed = { _, _ in
                Task { await event.signal() }
            }
            await event.wait()
        }
        return try getResults()
    }
}

As it suggested, you can use try await result?.get() to await for the results.

Alternatively, you can also pass a completion handler closure to result.completed without using Swift Concurrency.

1 Like

Thanks a lot @stevapple !!
We can effectively await on for the results calling the async get().

I think the only thing missing now to display the dialog is finding a way to initialize the fileOpenPicker instance with the actual window.

The following code shows the invalid handle error in the terminal.
I can't figure how to call WinRT.Interop in Swift.

Any idea ?

import Foundation
import UWP
import WinAppSDK
import WindowsFoundation
import WinUI
import WinSDK

@main
public class PreviewApp: SwiftApplication {
    override public func onLaunched(_ args: WinUI.LaunchActivatedEventArgs) {
        let window = Window()
        window.title = "Test FileOpenPicker"

        try! window.activate()

        let button = Button()
        button.content = "Open File"
        button.click.addHandler { _, _ in
            print("button clicked")
            let fileOpenPicker: FileOpenPicker = FileOpenPicker()
            fileOpenPicker.fileTypeFilter.append("*")
            fileOpenPicker.viewMode = .thumbnail
            fileOpenPicker.suggestedStartLocation = .desktop
            
            //
            // we need to initialize fileOpenPicker with the window
            //
            // with C# and WinUI using WinRT.Interop we can do:
            // var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
            // WinRT.Interop.InitializeWithWindow.Initialize(filePicker, hwnd);
            //

            // get the windowHandle via WinSDK - a correct workaround?
            let hWnd = WinSDK.GetActiveWindow()

            //
            // FiXME: How to initialize fileOpenPicker with the window handle ?
            //
            // How to call WinRT.Interop from Swift ?     
            // WinRT.Interop.InitializeWithWindow.Initialize(fileOpenPicker, hwnd)

            Task { @MainActor in
                    do {
                        let results = try fileOpenPicker.pickSingleFileAsync()
                        if let results {
                            do {
                                let file = try await results.get()
                                print("filename: \(file?.name)")
                            }
                            catch { print("results.get() error: \(error)") }
                        }
                    }
                    catch { print("pickOpenFileAsync error: \(error)") }
            }
        }

        let panel = StackPanel()
        panel.orientation = .vertical
        panel.spacing = 10
        panel.horizontalAlignment = .center
        panel.verticalAlignment = .center
        panel.children.append(button)
        window.content = panel
    }
}

Yes ! Using their great packages.