Skip to content

[pigeon] Support Swift Concurrency style api #147283

@yuukiw00w

Description

@yuukiw00w

Use case

I would like to be able to generate code in the format suitable for Swift Concurrency with pigeon.
When targeting the latest Swift version and iOS 13 or above, I want to generate an interface with Flutter in the form of new APIs that conform to MainActor, Sendable, async/await, and ExistentialAny.

related issue

#140439
#147127

Proposal

Assuming there is code targeted for generation as follows:

@HostApi()
abstract class SampleApi {
  @async
  Sample fetchSampleAsync(SampleParameter parameter);

  Sample fetchSampleSync(SampleParameter parameter);

  @async
  Object? objectSampleAsync(Object? parameter);

  Object? objectSampleSync(Object? parameter);
}

@FlutterApi()
abstract class CallFromNative {
  Sample fetchSample(SampleParameter parameter);

  Object? objectSample(Object? parameter);
}

When generating from this code, I would like to generate the following protocols:

@available(iOS 13, *)
@MainActor
protocol SampleApi {
  func fetchSampleAsync(parameter: SampleParameter) async -> Result<Sample, any Error>
  func fetchSampleSync(parameter: SampleParameter) throws -> Sample
  func objectSampleAsync(parameter: Any?) async -> Result<Any?, any Error>
  func objectSampleSync(parameter: Any?) throws -> Any?
}

@available(iOS, obsoleted: 13.0, renamed: "SampleApi")
protocol SampleApiLegacy {
  func fetchSampleAsync(parameter: SampleParameter, completion: @escaping (Result<Sample, any Error>) -> Void)
  func fetchSampleSync(parameter: SampleParameter) throws -> Sample
  func objectSampleAsync(parameter: Any?, completion: @escaping (Result<Any?, any Error>) -> Void)
  func objectSampleSync(parameter: Any?) throws -> Any?
}

@available(iOS 13, *)
@MainActor
protocol CallFromNativeProtocol {
  func fetchSample(parameter parameterArg: SampleParameter) async -> Result<Sample, FlutterError>
  func objectSample(parameter parameterArg: Any?) async -> Result<Any?, FlutterError>
}

@available(iOS, obsoleted: 13.0, renamed: "CallFromNativeProtocol")
protocol CallFromNativeProtocolLegacy {
  func fetchSample(parameter parameterArg: SampleParameter, completion: @escaping (Result<Sample, FlutterError>) -> Void)
  func objectSample(parameter parameterArg: Any?, completion: @escaping (Result<Any?, FlutterError>) -> Void)
}

Since I think it might be confusing to switch between Swift versions and iOS versions, I am attaching the expected generated code that I have implemented below.

All expected generated swift code
import Foundation

#if os(iOS)
  import Flutter
#elseif os(macOS)
  import FlutterMacOS
#else
  #error("Unsupported platform.")
#endif

#if swift(>=5.10)
private func wrapResult(_ result: Any?) -> [Any?] {
  return [result]
}

private func wrapError(_ error: Any) -> [Any?] {
  if let flutterError = error as? FlutterError {
    return [
      flutterError.code,
      flutterError.message,
      flutterError.details,
    ]
  }
  return [
    "\(error)",
    "\(type(of: error))",
    "Stacktrace: \(Thread.callStackSymbols)",
  ]
}

private func createConnectionError(withChannelName channelName: String) -> FlutterError {
  return FlutterError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "")
}

private func isNullish(_ value: Any?) -> Bool {
  return value is NSNull || value == nil
}

private func nilOrValue<T>(_ value: Any?) -> T? {
  if value is NSNull { return nil }
  return value as! T?
}

/// Generated class from Pigeon that represents data sent in messages.
struct Sample: Sendable {
  var text: String
  var id: Int64

  static func fromList(_ list: [Any?]) -> Sample? {
    let text = list[0] as! String
    let id = list[1] is Int64 ? list[1] as! Int64 : Int64(list[1] as! Int32)

    return Sample(
      text: text,
      id: id
    )
  }
  func toList() -> [Any?] {
    return [
      text,
      id,
    ]
  }
}

/// Generated class from Pigeon that represents data sent in messages.
struct SampleParameter: Sendable {
  var text: String
  var id: Int64

  static func fromList(_ list: [Any?]) -> SampleParameter? {
    let text = list[0] as! String
    let id = list[1] is Int64 ? list[1] as! Int64 : Int64(list[1] as! Int32)

    return SampleParameter(
      text: text,
      id: id
    )
  }
  func toList() -> [Any?] {
    return [
      text,
      id,
    ]
  }
}

private class SampleApiCodecReader: FlutterStandardReader {
  override func readValue(ofType type: UInt8) -> Any? {
    switch type {
    case 128:
      return Sample.fromList(self.readValue() as! [Any?])
    case 129:
      return SampleParameter.fromList(self.readValue() as! [Any?])
    default:
      return super.readValue(ofType: type)
    }
  }
}

private class SampleApiCodecWriter: FlutterStandardWriter {
  override func writeValue(_ value: Any) {
    if let value = value as? Sample {
      super.writeByte(128)
      super.writeValue(value.toList())
    } else if let value = value as? SampleParameter {
      super.writeByte(129)
      super.writeValue(value.toList())
    } else {
      super.writeValue(value)
    }
  }
}

private class SampleApiCodecReaderWriter: FlutterStandardReaderWriter {
  override func reader(with data: Data) -> FlutterStandardReader {
    return SampleApiCodecReader(data: data)
  }

  override func writer(with data: NSMutableData) -> FlutterStandardWriter {
    return SampleApiCodecWriter(data: data)
  }
}

class SampleApiCodec: FlutterStandardMessageCodec, @unchecked Sendable {
  static let shared = SampleApiCodec(readerWriter: SampleApiCodecReaderWriter())
}

/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
@available(iOS 13, *)
@MainActor
protocol SampleApi {
  func fetchSampleAsync(parameter: SampleParameter) async -> Result<Sample, any Error>
  func fetchSampleSync(parameter: SampleParameter) throws -> Sample
  func objectSampleAsync(parameter: Any?) async -> Result<Any?, any Error>
  func objectSampleSync(parameter: Any?) throws -> Any?
}

@available(iOS, obsoleted: 13.0, renamed: "SampleApi")
protocol SampleApiLegacy {
  func fetchSampleAsync(parameter: SampleParameter, completion: @escaping (Result<Sample, any Error>) -> Void)
  func fetchSampleSync(parameter: SampleParameter) throws -> Sample
  func objectSampleAsync(parameter: Any?, completion: @escaping (Result<Any?, any Error>) -> Void)
  func objectSampleSync(parameter: Any?) throws -> Any?
}

/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
class SampleApiSetup {
  /// The codec used by SampleApi.
  static var codec: FlutterStandardMessageCodec { SampleApiCodec.shared }
  /// Sets up an instance of `SampleApi` to handle messages through the `binaryMessenger`.
  @available(iOS 13, *)
  static func setUp(binaryMessenger: any FlutterBinaryMessenger, api: (any SampleApi)?, messageChannelSuffix: String = "") {
    let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
    let fetchSampleAsyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.fetchSampleAsync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      fetchSampleAsyncChannel.setMessageHandler { message, reply in
        Task { @MainActor in
          let args = message as! [Any?]
          let parameterArg = args[0] as! SampleParameter
          let result = await api.fetchSampleAsync(parameter: parameterArg)
          switch result {
          case .success(let res):
            reply(wrapResult(res))
          case .failure(let error):
            reply(wrapError(error))
          }
        }
      }
    } else {
      fetchSampleAsyncChannel.setMessageHandler(nil)
    }
    let fetchSampleSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.fetchSampleSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      fetchSampleSyncChannel.setMessageHandler { message, reply in
        MainActor.assumeIsolated {
          let args = message as! [Any?]
          let parameterArg = args[0] as! SampleParameter
          do {
            let result = try api.fetchSampleSync(parameter: parameterArg)
            reply(wrapResult(result))
          } catch {
            reply(wrapError(error))
          }
        }
      }
    } else {
      fetchSampleSyncChannel.setMessageHandler(nil)
    }
    let objectSampleAsyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.objectSampleAsync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      objectSampleAsyncChannel.setMessageHandler { message, reply in
        Task { @MainActor in
          let args = message as! [Any?]
          let parameterArg: Any? = args[0]
          let result = await api.objectSampleAsync(parameter: parameterArg)
          switch result {
          case .success(let res):
            reply(wrapResult(res))
          case .failure(let error):
            reply(wrapError(error))
          }
        }
      }
    } else {
      objectSampleAsyncChannel.setMessageHandler(nil)
    }
    let objectSampleSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.objectSampleSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      objectSampleSyncChannel.setMessageHandler { message, reply in
        MainActor.assumeIsolated {
          let args = message as! [Any?]
          let parameterArg: Any? = args[0]
          do {
            let result = try api.objectSampleSync(parameter: parameterArg)
            reply(wrapResult(result))
          } catch {
            reply(wrapError(error))
          }
        }
      }
    } else {
      objectSampleSyncChannel.setMessageHandler(nil)
    }
  }
  
  /// Sets up an instance of `SampleApiLegacy` to handle messages through the `binaryMessenger`.
  @available(iOS, obsoleted: 13.0)
  static func setUp(binaryMessenger: any FlutterBinaryMessenger, api: (any SampleApiLegacy)?, messageChannelSuffix: String = "") {
    let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
    let fetchSampleAsyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.fetchSampleAsync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      fetchSampleAsyncChannel.setMessageHandler { message, reply in
        let args = message as! [Any?]
        let parameterArg = args[0] as! SampleParameter
        api.fetchSampleAsync(parameter: parameterArg) { result in
          switch result {
          case .success(let res):
            reply(wrapResult(res))
          case .failure(let error):
            reply(wrapError(error))
          }
        }
      }
    } else {
      fetchSampleAsyncChannel.setMessageHandler(nil)
    }
    let fetchSampleSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.fetchSampleSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      fetchSampleSyncChannel.setMessageHandler { message, reply in
        let args = message as! [Any?]
        let parameterArg = args[0] as! SampleParameter
        do {
          let result = try api.fetchSampleSync(parameter: parameterArg)
          reply(wrapResult(result))
        } catch {
          reply(wrapError(error))
        }
      }
    } else {
      fetchSampleSyncChannel.setMessageHandler(nil)
    }
    let objectSampleAsyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.objectSampleAsync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      objectSampleAsyncChannel.setMessageHandler { message, reply in
        let args = message as! [Any?]
        let parameterArg: Any? = args[0]
        api.objectSampleAsync(parameter: parameterArg) { result in
          switch result {
          case .success(let res):
            reply(wrapResult(res))
          case .failure(let error):
            reply(wrapError(error))
          }
        }
      }
    } else {
      objectSampleAsyncChannel.setMessageHandler(nil)
    }
    let objectSampleSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.objectSampleSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      objectSampleSyncChannel.setMessageHandler { message, reply in
        let args = message as! [Any?]
        let parameterArg: Any? = args[0]
        do {
          let result = try api.objectSampleSync(parameter: parameterArg)
          reply(wrapResult(result))
        } catch {
          reply(wrapError(error))
        }
      }
    } else {
      objectSampleSyncChannel.setMessageHandler(nil)
    }
  }
}
private class CallFromNativeCodecReader: FlutterStandardReader {
  override func readValue(ofType type: UInt8) -> Any? {
    switch type {
    case 128:
      return Sample.fromList(self.readValue() as! [Any?])
    case 129:
      return SampleParameter.fromList(self.readValue() as! [Any?])
    default:
      return super.readValue(ofType: type)
    }
  }
}

private class CallFromNativeCodecWriter: FlutterStandardWriter {
  override func writeValue(_ value: Any) {
    if let value = value as? Sample {
      super.writeByte(128)
      super.writeValue(value.toList())
    } else if let value = value as? SampleParameter {
      super.writeByte(129)
      super.writeValue(value.toList())
    } else {
      super.writeValue(value)
    }
  }
}

private class CallFromNativeCodecReaderWriter: FlutterStandardReaderWriter {
  override func reader(with data: Data) -> FlutterStandardReader {
    return CallFromNativeCodecReader(data: data)
  }

  override func writer(with data: NSMutableData) -> FlutterStandardWriter {
    return CallFromNativeCodecWriter(data: data)
  }
}

class CallFromNativeCodec: FlutterStandardMessageCodec, @unchecked Sendable {
  static let shared = CallFromNativeCodec(readerWriter: CallFromNativeCodecReaderWriter())
}

/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
@available(iOS 13, *)
@MainActor
protocol CallFromNativeProtocol {
  func fetchSample(parameter parameterArg: SampleParameter) async -> Result<Sample, FlutterError>
  func objectSample(parameter parameterArg: Any?) async -> Result<Any?, FlutterError>
}

/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
@available(iOS, obsoleted: 13.0)
protocol CallFromNativeProtocolLegacy {
  func fetchSample(parameter parameterArg: SampleParameter, completion: @escaping (Result<Sample, FlutterError>) -> Void)
  func objectSample(parameter parameterArg: Any?, completion: @escaping (Result<Any?, FlutterError>) -> Void)
}

@available(iOS 13, *)
final class CallFromNative: CallFromNativeProtocol {
  private let binaryMessenger: any FlutterBinaryMessenger
  private let messageChannelSuffix: String
  init(binaryMessenger: any FlutterBinaryMessenger, messageChannelSuffix: String = "") {
    self.binaryMessenger = binaryMessenger
    self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
  }
  var codec: FlutterStandardMessageCodec {
    return CallFromNativeCodec.shared
  }
  func fetchSample(parameter parameterArg: SampleParameter) async -> Result<Sample, FlutterError> {
    await withCheckedContinuation { continuation in
      let channelName: String = "dev.flutter.pigeon.modern_swift.CallFromNative.fetchSample\(messageChannelSuffix)"
      let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
      channel.sendMessage([parameterArg] as [Any?]) { response in
        MainActor.assumeIsolated {
          guard let listResponse = response as? [Any?] else {
            continuation.resume(returning: .failure(createConnectionError(withChannelName: channelName)))
            return
          }
          if listResponse.count > 1 {
            let code: String = listResponse[0] as! String
            let message: String? = nilOrValue(listResponse[1])
            let details: String? = nilOrValue(listResponse[2])
            continuation.resume(returning: .failure(FlutterError(code: code, message: message, details: details)))
          } else if listResponse[0] == nil {
            continuation.resume(returning: .failure(FlutterError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))
          } else {
            let result = listResponse[0] as! Sample
            continuation.resume(returning: .success(result))
          }
        }
      }
    }
  }
  func objectSample(parameter parameterArg: Any?) async -> Result<Any?, FlutterError> {
    await withCheckedContinuation { continuation in
      let channelName: String = "dev.flutter.pigeon.modern_swift.CallFromNative.objectSample\(messageChannelSuffix)"
      let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
      channel.sendMessage([parameterArg] as [Any?]) { response in
        guard let listResponse = response as? [Any?] else {
          continuation.resume(returning: .failure(createConnectionError(withChannelName: channelName)))
          return
        }
        if listResponse.count > 1 {
          let code: String = listResponse[0] as! String
          let message: String? = nilOrValue(listResponse[1])
          let details: String? = nilOrValue(listResponse[2])
          continuation.resume(returning: .failure(FlutterError(code: code, message: message, details: details)))
        } else {
          let result: Any? = listResponse[0]
          continuation.resume(returning: .success(result))
        }
      }
    }
  }
}

@available(iOS, obsoleted: 13.0)
class CallFromNativeLegacy: CallFromNativeProtocolLegacy {
  private let binaryMessenger: any FlutterBinaryMessenger
  private let messageChannelSuffix: String
  init(binaryMessenger: any FlutterBinaryMessenger, messageChannelSuffix: String = "") {
    self.binaryMessenger = binaryMessenger
    self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
  }
  var codec: FlutterStandardMessageCodec {
    return CallFromNativeCodec.shared
  }
  func fetchSample(parameter parameterArg: SampleParameter, completion: @escaping (Result<Sample, FlutterError>) -> Void) {
    let channelName: String = "dev.flutter.pigeon.modern_swift.CallFromNative.fetchSample\(messageChannelSuffix)"
    let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
    channel.sendMessage([parameterArg] as [Any?]) { response in
      guard let listResponse = response as? [Any?] else {
        completion(.failure(createConnectionError(withChannelName: channelName)))
        return
      }
      if listResponse.count > 1 {
        let code: String = listResponse[0] as! String
        let message: String? = nilOrValue(listResponse[1])
        let details: String? = nilOrValue(listResponse[2])
        completion(.failure(FlutterError(code: code, message: message, details: details)))
      } else if listResponse[0] == nil {
        completion(.failure(FlutterError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))
      } else {
        let result = listResponse[0] as! Sample
        completion(.success(result))
      }
    }
  }
  func objectSample(parameter parameterArg: Any?, completion: @escaping (Result<Any?, FlutterError>) -> Void) {
    let channelName: String = "dev.flutter.pigeon.modern_swift.CallFromNative.objectSample\(messageChannelSuffix)"
    let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
    channel.sendMessage([parameterArg] as [Any?]) { response in
      guard let listResponse = response as? [Any?] else {
        completion(.failure(createConnectionError(withChannelName: channelName)))
        return
      }
      if listResponse.count > 1 {
        let code: String = listResponse[0] as! String
        let message: String? = nilOrValue(listResponse[1])
        let details: String? = nilOrValue(listResponse[2])
        completion(.failure(FlutterError(code: code, message: message, details: details)))
      } else {
        let result: Any? = listResponse[0]
        completion(.success(result))
      }
    }
  }
}
#else
private func wrapResult(_ result: Any?) -> [Any?] {
  return [result]
}

private func wrapError(_ error: Any) -> [Any?] {
  if let flutterError = error as? FlutterError {
    return [
      flutterError.code,
      flutterError.message,
      flutterError.details,
    ]
  }
  return [
    "\(error)",
    "\(type(of: error))",
    "Stacktrace: \(Thread.callStackSymbols)",
  ]
}

private func createConnectionError(withChannelName channelName: String) -> FlutterError {
  return FlutterError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "")
}

private func isNullish(_ value: Any?) -> Bool {
  return value is NSNull || value == nil
}

private func nilOrValue<T>(_ value: Any?) -> T? {
  if value is NSNull { return nil }
  return value as! T?
}

/// Generated class from Pigeon that represents data sent in messages.
struct Sample {
  var text: String
  var id: Int64

  static func fromList(_ list: [Any?]) -> Sample? {
    let text = list[0] as! String
    let id = list[1] is Int64 ? list[1] as! Int64 : Int64(list[1] as! Int32)

    return Sample(
      text: text,
      id: id
    )
  }
  func toList() -> [Any?] {
    return [
      text,
      id,
    ]
  }
}

/// Generated class from Pigeon that represents data sent in messages.
struct SampleParameter {
  var text: String
  var id: Int64

  static func fromList(_ list: [Any?]) -> SampleParameter? {
    let text = list[0] as! String
    let id = list[1] is Int64 ? list[1] as! Int64 : Int64(list[1] as! Int32)

    return SampleParameter(
      text: text,
      id: id
    )
  }
  func toList() -> [Any?] {
    return [
      text,
      id,
    ]
  }
}

private class SampleApiCodecReader: FlutterStandardReader {
  override func readValue(ofType type: UInt8) -> Any? {
    switch type {
    case 128:
      return Sample.fromList(self.readValue() as! [Any?])
    case 129:
      return SampleParameter.fromList(self.readValue() as! [Any?])
    default:
      return super.readValue(ofType: type)
    }
  }
}

private class SampleApiCodecWriter: FlutterStandardWriter {
  override func writeValue(_ value: Any) {
    if let value = value as? Sample {
      super.writeByte(128)
      super.writeValue(value.toList())
    } else if let value = value as? SampleParameter {
      super.writeByte(129)
      super.writeValue(value.toList())
    } else {
      super.writeValue(value)
    }
  }
}

private class SampleApiCodecReaderWriter: FlutterStandardReaderWriter {
  override func reader(with data: Data) -> FlutterStandardReader {
    return SampleApiCodecReader(data: data)
  }

  override func writer(with data: NSMutableData) -> FlutterStandardWriter {
    return SampleApiCodecWriter(data: data)
  }
}

class SampleApiCodec: FlutterStandardMessageCodec {
  static let shared = SampleApiCodec(readerWriter: SampleApiCodecReaderWriter())
}

/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol SampleApi {
  func fetchSampleAsync(parameter: SampleParameter, completion: @escaping (Result<Sample, Error>) -> Void)
  func fetchSampleSync(parameter: SampleParameter) throws -> Sample
  func objectSampleAsync(parameter: Any?, completion: @escaping (Result<Any?, Error>) -> Void)
  func objectSampleSync(parameter: Any?) throws -> Any?
}

/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
class SampleApiSetup {
  /// The codec used by SampleApi.
  static var codec: FlutterStandardMessageCodec { SampleApiCodec.shared }
  /// Sets up an instance of `SampleApi` to handle messages through the `binaryMessenger`.
  static func setUp(binaryMessenger: FlutterBinaryMessenger, api: SampleApi?, messageChannelSuffix: String = "") {
    let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
    let fetchSampleAsyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.fetchSampleAsync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      fetchSampleAsyncChannel.setMessageHandler { message, reply in
        let args = message as! [Any?]
        let parameterArg = args[0] as! SampleParameter
        api.fetchSampleAsync(parameter: parameterArg) { result in
          switch result {
          case .success(let res):
            reply(wrapResult(res))
          case .failure(let error):
            reply(wrapError(error))
          }
        }
      }
    } else {
      fetchSampleAsyncChannel.setMessageHandler(nil)
    }
    let fetchSampleSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.fetchSampleSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      fetchSampleSyncChannel.setMessageHandler { message, reply in
        let args = message as! [Any?]
        let parameterArg = args[0] as! SampleParameter
        do {
          let result = try api.fetchSampleSync(parameter: parameterArg)
          reply(wrapResult(result))
        } catch {
          reply(wrapError(error))
        }
      }
    } else {
      fetchSampleSyncChannel.setMessageHandler(nil)
    }
    let objectSampleAsyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.objectSampleAsync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      objectSampleAsyncChannel.setMessageHandler { message, reply in
        let args = message as! [Any?]
        let parameterArg: Any? = args[0]
        api.objectSampleAsync(parameter: parameterArg) { result in
          switch result {
          case .success(let res):
            reply(wrapResult(res))
          case .failure(let error):
            reply(wrapError(error))
          }
        }
      }
    } else {
      objectSampleAsyncChannel.setMessageHandler(nil)
    }
    let objectSampleSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.modern_swift.SampleApi.objectSampleSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
    if let api = api {
      objectSampleSyncChannel.setMessageHandler { message, reply in
        let args = message as! [Any?]
        let parameterArg: Any? = args[0]
        do {
          let result = try api.objectSampleSync(parameter: parameterArg)
          reply(wrapResult(result))
        } catch {
          reply(wrapError(error))
        }
      }
    } else {
      objectSampleSyncChannel.setMessageHandler(nil)
    }
  }
}
private class CallFromNativeCodecReader: FlutterStandardReader {
  override func readValue(ofType type: UInt8) -> Any? {
    switch type {
    case 128:
      return Sample.fromList(self.readValue() as! [Any?])
    case 129:
      return SampleParameter.fromList(self.readValue() as! [Any?])
    default:
      return super.readValue(ofType: type)
    }
  }
}

private class CallFromNativeCodecWriter: FlutterStandardWriter {
  override func writeValue(_ value: Any) {
    if let value = value as? Sample {
      super.writeByte(128)
      super.writeValue(value.toList())
    } else if let value = value as? SampleParameter {
      super.writeByte(129)
      super.writeValue(value.toList())
    } else {
      super.writeValue(value)
    }
  }
}

private class CallFromNativeCodecReaderWriter: FlutterStandardReaderWriter {
  override func reader(with data: Data) -> FlutterStandardReader {
    return CallFromNativeCodecReader(data: data)
  }

  override func writer(with data: NSMutableData) -> FlutterStandardWriter {
    return CallFromNativeCodecWriter(data: data)
  }
}

class CallFromNativeCodec: FlutterStandardMessageCodec {
  static let shared = CallFromNativeCodec(readerWriter: CallFromNativeCodecReaderWriter())
}

/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
protocol CallFromNativeProtocol {
  func fetchSample(parameter parameterArg: SampleParameter, completion: @escaping (Result<Sample, FlutterError>) -> Void)
  func objectSample(parameter parameterArg: Any?, completion: @escaping (Result<Any?, FlutterError>) -> Void)
}
class CallFromNative: CallFromNativeProtocol {
  private let binaryMessenger: FlutterBinaryMessenger
  private let messageChannelSuffix: String
  init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") {
    self.binaryMessenger = binaryMessenger
    self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
  }
  var codec: FlutterStandardMessageCodec {
    return CallFromNativeCodec.shared
  }
  func fetchSample(parameter parameterArg: SampleParameter, completion: @escaping (Result<Sample, FlutterError>) -> Void) {
    let channelName: String = "dev.flutter.pigeon.modern_swift.CallFromNative.fetchSample\(messageChannelSuffix)"
    let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
    channel.sendMessage([parameterArg] as [Any?]) { response in
      guard let listResponse = response as? [Any?] else {
        completion(.failure(createConnectionError(withChannelName: channelName)))
        return
      }
      if listResponse.count > 1 {
        let code: String = listResponse[0] as! String
        let message: String? = nilOrValue(listResponse[1])
        let details: String? = nilOrValue(listResponse[2])
        completion(.failure(FlutterError(code: code, message: message, details: details)))
      } else if listResponse[0] == nil {
        completion(.failure(FlutterError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))
      } else {
        let result = listResponse[0] as! Sample
        completion(.success(result))
      }
    }
  }
  func objectSample(parameter parameterArg: Any?, completion: @escaping (Result<Any?, FlutterError>) -> Void) {
    let channelName: String = "dev.flutter.pigeon.modern_swift.CallFromNative.objectSample\(messageChannelSuffix)"
    let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
    channel.sendMessage([parameterArg] as [Any?]) { response in
      guard let listResponse = response as? [Any?] else {
        completion(.failure(createConnectionError(withChannelName: channelName)))
        return
      }
      if listResponse.count > 1 {
        let code: String = listResponse[0] as! String
        let message: String? = nilOrValue(listResponse[1])
        let details: String? = nilOrValue(listResponse[2])
        completion(.failure(FlutterError(code: code, message: message, details: details)))
      } else {
        let result: Any? = listResponse[0]
        completion(.success(result))
      }
    }
  }
}
#endif 
use case sample code
// in AppDelegate
@main
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
      _ application: UIApplication,
      didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        guard let controller: FlutterViewController = window?.rootViewController as? FlutterViewController else {
            return super.application(application, didFinishLaunchingWithOptions: launchOptions)
        }
        
        GeneratedPluginRegistrant.register(with: self)
        
        let binaryMessanger = controller.binaryMessenger
        if #available(iOS 13, *) {
            let callFromNative = CallFromNative(binaryMessenger: binaryMessanger)
            let sampleApi = SampleApiImpl(callFromNative: callFromNative)
            SampleApiSetup.setUp(binaryMessenger: binaryMessanger, api: sampleApi)
        } else {
            let callFromNative = CallFromNativeLegacy(binaryMessenger: binaryMessanger)
            let sampleApi = SampleApiLegacyImpl(callFromNative: callFromNative)
            SampleApiSetup.setUp(binaryMessenger: binaryMessanger, api: sampleApi)
        }
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

// in sample api implement class
extension FlutterError: Error, @unchecked Sendable {}

@available(iOS 13, *)
@MainActor
final class SampleApiImpl: SampleApi {
    let fetcher = SampleDataFetcher()
    let callFromNative: CallFromNative
    
    init(callFromNative: CallFromNative) {
        self.callFromNative = callFromNative
    }
    
    func fetchSampleAsync(parameter: SampleParameter) async -> Result<Sample, any Error> {
        let sample = await fetcher.fetchSampleAsync()
        let result = await callFromNative.fetchSample(parameter: .init(text: sample.text, id: sample.id))
        switch result {
        case .success(let newSample):
            return .success(newSample)
        case .failure(let failure):
            return .failure(failure)
        }
    }
    
    func fetchSampleSync(parameter: SampleParameter) throws -> Sample {
        let sample = fetcher.fetchSampleSync()
        print("fetchSampleSync test")
        return sample
    }
    
    func objectSampleAsync(parameter: Any?) async -> Result<Any?, any Error> {
        let testResult = await ObjectSampleTest.testAsync(parameter: parameter)
        return .success(parameter)
    }
    
    func objectSampleSync(parameter: Any?) throws -> Any? {
        parameter
    }
}

enum ObjectSampleTest {
    nonisolated static func testAsync(parameter: Any?) async -> Any? {
        parameter
    }
}

@available(iOS, obsoleted: 13.0)
final class SampleApiLegacyImpl: SampleApiLegacy {
    let callFromNative: CallFromNativeLegacy
    
    init(callFromNative: CallFromNativeLegacy) {
        self.callFromNative = callFromNative
    }
    
    func fetchSampleAsync(parameter: SampleParameter, completion: @escaping (Result<Sample, any Error>) -> Void) {
        let sample = Sample(text: "test", id: 1)
        callFromNative.fetchSample(
            parameter: .init(text: sample.text, id: sample.id)) { result in
                switch result {
                case .success(let newSample):
                    completion(.success(newSample))
                case .failure(let failure):
                    completion(.failure(failure))
                }
            }
    }
    
    func fetchSampleSync(parameter: SampleParameter) throws -> Sample {
        return Sample(text: "test3", id: 3)
    }
    
    func objectSampleAsync(parameter: Any?, completion: @escaping (Result<Any?, any Error>) -> Void) {
        completion(.success(parameter))
    }
    
    func objectSampleSync(parameter: Any?) throws -> Any? {
        parameter
    }
}

@MainActor
final class SampleDataFetcher {
    nonisolated func fetchSampleAsync() async -> Sample {
        Sample(text: "test", id: 1)
    }
    
    func fetchSampleSync() -> Sample {
        Sample(text: "test2", id: 2)
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterp: pigeonrelated to pigeon messaging codegen toolpackageflutter/packages repository. See also p: labels.team-ecosystemOwned by Ecosystem teamtriaged-ecosystemTriaged by Ecosystem team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions