βοΈ Featured on the official x-callback-url.com blog
A 100% type-safe API to the x-callback-url scheme.
Suppose we want to build this x-callback-url in Middleman:
target://x-callback-url/do-something?
key=value&
x-success=source://x-callback-url/success?
something=thing&
x-error=source://x-callback-url/error?
errorCode=404&
errorMessage=message
We first declare the App called "Target". Target's only purpose is to provide the url-scheme://. It also makes a good namespace for all your actions. An Action is comprised of two nested types: Input and Output, which must conform to Codable. There are further customization option that you will learn about later.
To run the action, you call run(action:with:then:) on the App. run wants to know the Action to run, the Input of that action and a closure that is called with a Response<Output> once a callback is registered.
struct Target: App {
struct DoSomething: Action {
struct Input: Codable {
let key: Value
let optional: Value?
let default: Value? = nil
}
struct Output: Codable {
let something: Thing
}
}
}
// Running the action
Target().run(
action: DoSomething(),
with: .init(
key: value,
optional: nil
),
then: { response in
switch response {
case let .success(output):
print(output?.something)
case let .error(code, msg):
print(code, msg)
case .cancel:
print("canceled")
}
}
)- Overhaul the receiving-urls-API so Middleman can be used to maintain x-callback APIs, not just work with existing ones
- Implement a command-line interface using
apple/swift-argument-parser - Migrate from callbacks to
asyncin Swift 6
- π― Honey uses Middleman to provide a swifty API for Bear's x-callback-url API
- File a pull request to include your own project!
If you want to receive callbacks you need to make sure your app has a custom url scheme implemented. Middleman will then read the first entry in the CFBundleURLTypes array in the main bundle's Info.plist. You can also manually define a url scheme.
For Middleman to be able to parse incoming urls, you need to put one of the following methods in the delegate (UIKit/Cocoa) appropriate for your platform or in the onOpenURL SwiftUI modifier.
// SwiftUI
// On any view (maybe in your `App`)
.onOpenURL { url in
Middleman.receive(url)
}
// macOS
// In your `NSAppDelegate`:
func application(_ application: NSApplication, open urls: [URL]) {
Middleman.receive(urls)
}
// iOS 13 and up
// In your `UISceneDelegate`:
func scene(_ scene: UIScene, openURLContexts urlContexts: Set<UIOpenURLContext>) {
Middleman.receive(urlContexts)
}
// iOS 12 and below
// In your `UIAppDelegate`:
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
Middleman.receive(url)
}If Middleman's default behavior of reading from the Info.plist file does not work for you, you can manually define your url scheme. You do so by setting Middleman.receiver to your custom implementation.
struct MyApp: Receiver {
var scheme: String { "my-scheme" }
}
// Then, notify Middleman of your custom implementation
Middleman.receiver = MyApp()Middleman is a Swift Package. Write this in your Package.swift file:
let package = Package(
...
dependencies: [
.package(url: "https://github.com/ValentinWalter/middleman.git", from: "1.0.0")
],
...
)- Define an
Action, representing an x-callback-url action. - Define an
App, which is responsible for sending and receiving actions. - Run actions via
App.run(action:)with theirInputassociated type, optionally providing a closure that receives the Action'sOutput.
An action in Middleman represents an x-callback-url action. You create an action by conforming to the Action protocol. This requires you to define an Input and Output, which themselves require conformance to Codable. By default, Middleman will infer the path name of the action to be the kebab-case equivalent of the name of the Action type. In the example below, this would result in "open-note". You can overwrite this behavior by implementing the path property into your Action.
// Shortened version of Bear's /open-note action
struct OpenNote: Action {
struct Input: Codable {
var title: String
var excludeTrashed: Bool
}
struct Output: Codable {
var note: String
var modificationDate: Date
}
}You can make handy use of typealias when it doesn't make sense to create your own type. Here we have an Action that takes a URL and has no output. Sometimes an Action doesn't have an Input or Output. In those cases, just typealias it to be Never and Middleman handles the rest.
struct OpenURL: Action {
typealias Input = URL
typealias Output = Never
}You can implement the receive(input:) method in your Action to customize the behavior when the action was received by Middleman. Note that you also need to include your receiving action in your Receiver's receivingActions property. This API is in an alpha state (see next steps).
struct OpenBook: Action {
...
func receive(input: Input) {
// Handle opening book
}
}Sending actions requires an App. You create one by conforming to the App protocol. Similarly to the Action protocol, Middleman infers the url-scheme of the app to be the kebab-case equivalent of the name of the conforming type. By default, the host property will be assumed to be "x-callback-url", as specified by the x-callback-url 1.0 DRAFT spec.
struct Bear: App {
// By default, Middleman infers the two properties as implemented below
var scheme: String { "bear" }
var host: String { "x-callback-url" }
}If your intent is to not only send, but receive actions, you define a Receiver, which inherits from the App protocol. This requires you to specify the actions with which your App can be opened. You then need to notify Middleman of your custom implementation, as described in Manually defining your url scheme. This API is in an alpha state (see next steps).
struct MyApp: Receiver {
var receivingActions = [
OpenBook().erased(),
AnotherAction().erased()
]
}Here's how running the above implementation of OpenNote would look.
Bear().run(
action: OpenNote(),
with: .init(
title: "Title",
excludeTrashed: true
),
then: { response in
switch response {
case let .success(output): print(output?.note)
case let .error(code, message): print(code, message)
case .cancel: print("canceled")
}
}
)In the case of an action having neither an Input or Output, you would have something like this:
SomeApp().run(
action: SomeAction(),
then: { response in
switch response {
case .success: print("success!")
case .error(let code, let msg): print(code, msg)
case .cancel: print("canceled")
}
}
)It's a good idea to namespace your actions in an extension of their App. You can then also define static convenience functions, as calling the run method can get quite verbose. Following the OpenNote example from above:
extension Bear {
// Namespaced declaration of the `OpenNote` action
struct OpenNote { ... }
// Static convenience function, making working with `OpenNote` more pleasant
static func openNote(
titled title: String,
excludeTrashed: Bool = false,
then callback: @escaping () -> Void
) {
Bear().run(
action: OpenNote(),
with: .init(
title: title,
excludeTrashed: excludeTrashed
),
then: { response in
switch response {
case .success(let output):
guard let output = output else { break }
callback(output.note)
case .error: break
case .cancel: break
}
}
)
}
}
// Opening a note is now as easy as
Bear.openNote(titled: "Title") { note in
print("\(note) π₯³")
}Middleman uses a custom Decoder to go from raw URL to your Action.Output. Dispatched actions are stored with a UUID that Middleman inserts to each x-success/x-error/x-cancel parameter to match actions and their stored callbacks.