βοΈ 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
async
in 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 theirInput
associated 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.