This is a Swift-based client SDK for OpenWhisk. You can use it to connect to the IBM Cloud Functions service, or you own installation of Apache OpenWhisk. It partially implements the OpenWhisk REST API and allows you to invoke actions and fire triggers. The client SDK is compatible with Swift 4.x and runs on iOS 11, WatchOS 4, and Darwin. Since this code uses classes like URLSession, Linux support is linked to the current status of Foundation on Linux.
You can install the SDK using the source code in this repo, as a Cocoapod for iOS and WatchOS apps, Carthage, and as a package using the Swift Package Manager for Darwin CLI apps.
To build the source code:
- Clone this repo.
- Open the
OpenWhisk.xcodeproj
file in Xcode 9.x - Build the OpenWhisk target for an iOS app or the OpenWhiskWatch target for a WatchOS app.
- Locate the binary framework file (usually in
debug
orrelease
directories at~/Library/Developer/Xcode/DerivedData/$projectName-*
) and add it to the "Embedded Binaries" list in the General settings of your apps' target.
The official CocoaPods website has detailed instructions on how to install and use CocoaPods.
The following lines in a Podfile will install the SDK for an iOS app with a watch OS extension:
install! 'cocoapods', :deterministic_uuids => false
use_frameworks!
target 'MyApp' do
pod 'OpenWhisk', :git => 'https://github.com/apache/incubator-openwhisk-client-swift.git', :tag => '0.3.0'
end
target 'MyApp WatchKit Extension' do
pod 'OpenWhisk', :git => 'https://github.com/apache/incubator-openwhisk-client-swift.git', :tag => '0.3.0'
end
You may get the warning 'target overrides the EMBEDDED_CONTENT_CONTAINS_SWIFT
' when you have a watch target. You can eliminate this warning by changing this setting in "Build Settings" to the value '$(inherited)'.
After installation, open your project workspace. You may get the following warning when building:
Use Legacy Swift Language Version” (SWIFT_VERSION) is required to be configured correctly for targets which use Swift. Use the [Edit > Convert > To Current Swift Syntax…] menu to choose a Swift version or use the Build Settings editor to configure the build setting directly.
This is caused if Cocoapods does not update the Swift version in the Pods project. To fix, select the Pods project and the OpenWhisk target. Go to Build Settings and change the setting Use Legacy Swift Language Version
to no
. You can also add the following post installation instructions at the end of you Podfile:
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '4.0'
end
end
end
Visit the official Carthage site on Github for detailed instructions on installing and using Carthage.
Here is an example Cartfile for iOS installation using Carthage:
github "openwhisk/openwhisk-client-swift.git" ~> 0.3.0 # Or latest version
Use the Swift Package Manager to install into a Darwin CLI app. Below is an example Package.swift manifest file you can use:
import PackageDescription
let package = Package(
name: "PackageTest",
dependencies: [
.Package(url: "https://github.com/apache/incubator-openwhisk-client-swift.git", versions: Version(0,0,0)..<Version(1,0,0)),
]
)
To get up and running quickly, create a WhiskCredentials object with your OpenWhisk API credentials and create a Whisk instance from that.
You create a credentials object as:
let credentialsConfiguration = WhiskCredentials(accessKey: "myKey", accessToken: "myToken")
let whisk = Whisk(credentials: credentialsConfiguration!)
You can retrieve the key and token with the following CLI command:
wsk property get --auth
whisk auth kkkkkkkk-kkkk-kkkk-kkkk-kkkkkkkkkkkk:tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt
The strings before and after the colon are your key and token, respectively.
Call "invokeAction" with the action name to invoke a remote action. You can specify the namespace the action belongs to, or just leave it blank to accept the default namespace. Use a dictionary to pass parameters to the action as required.
// In this example, we are invoking an action to print a message to the OpenWhisk Console
var params = Dictionary<String, String>()
params["payload"] = "Hi from mobile"
do {
try whisk.invokeAction(name: "helloConsole", package: "mypackage", namespace: "mynamespace", parameters: params, hasResult: false, callback: {(reply, error) -> Void in
if let error = error {
//do something
print("Error invoking action \(error.localizedDescription)")
} else {
print("Action invoked!")
}
})
} catch {
print("Error \(error)")
}
In the above example, we are invoking the "helloConsole" action using the default namespace.
To fire a remote OpenWhisk trigger, call the "fireTrigger" method. Pass in parameters as required using a dictionary.
// In this example we are firing a trigger when our location has changed by a certain amount
var locationParams = Dictionary<String, String>()
locationParams["payload"] = "{\"lat\":41.27093, \"lon\":-73.77763}"
do {
try whisk.fireTrigger(name: "locationChanged", package: "mypackage", namespace: "mynamespace", parameters: locationParams, callback: {(reply, error) -> Void in
if let error = error {
print("Error firing trigger \(error.localizedDescription)")
} else {
print("Trigger fired!")
}
})
} catch {
print("Error \(error)")
}
In the above example, we are firing a trigger "locationChanged".
If the action returns a result, set hasResult to true in the invokeAction call. The result of the action is returned in the reply dictionary, for example:
do {
try whisk.invokeAction(name: "actionWithResult", package: "mypackage", namespace: "mynamespace", parameters: params, hasResult: true, callback: {(reply, error) -> Void in
if let error = error {
//do something
print("Error invoking action \(error.localizedDescription)")
} else {
var result = reply["result"]
print("Got result \(result)")
}
})
} catch {
print("Error \(error)")
}
By default, the SDK will only return the activationId and any result produced by the invoked action. To get metadata of the entire response object, which includes the HTTP response status code and the REST API URL the SDK tried to call, use this setting:
whisk.verboseReplies = true
The REST API URL called will be in the actionUrl/triggerUrl fields of the response.
You can configure the SDK to work with different installations of OpenWhisk using the baseURL parameter. For instance:
whisk.baseURL = "http://localhost:8080"
will use an OpenWhisk running at localhost:8080. If you do not specify the baseUrl, the Mobile SDK will use the instance running at https://openwhisk.ng.bluemix.net
You can pass in a custom URLSession in case you require special network handling. For example, you may have your own OpenWhisk installation that uses self-signed certificates:
// create a network delegate that trusts everything
class NetworkUtilsDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
}
// create an URLSession that uses the trusting delegate
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: NetworkUtilsDelegate(), delegateQueue:OperationQueue.main)
// set the SDK to use this urlSession instead of the default shared one
whisk.urlSession = session
All actions and triggers have a fully qualified name which is made up of a namespace, a package, and an action/trigger name. The SDK can accept these as parameters when invoking an action or firing a trigger. The SDK also provides a function that accepts a fully qualified name that looks like "/mynamespace/mypackage/nameOfActionOrTrigger". The qualified name string supports unnamed default values for namespaces and packages that all OpenWhisk users have, so the following parsing rules apply:
- qName = "foo" will result in namespace = default, package = default, action/trigger = "foo"
- qName = "mypackage/foo" will result in namespace = default, package = mypackage, action/trigger = "foo"
- qName = "/mynamespace/foo" will result in namespace = mynamespace, package = default, action/trigger = "foo"
- qName = "/mynamespace/mypackage/foo will result in namespace = mynamespace, package = mypackage, action/trigger = "foo"
All other combinations will throw a WhiskError.QualifiedName error. When using qualified names, you must wrap the call in a do/try/catch block.
For convenience, the iOS version of the SDK includes a WhiskButton, which extends the UIButton to allow it to invoke OpenWhisk actions. To use this:
var whiskButton = WhiskButton(frame: CGRectMake(0,0,20,20))
whiskButton.setupWhiskAction("helloConsole", package: "mypackage", namespace: "_", credentials: credentialsConfiguration!, hasResult: false, parameters: nil, urlSession: nil)
let myParams = ["name":"value"]
// Call this when you detect a press event, e.g. in an IBAction, to invoke the action
whiskButton.invokeAction(parameters: myParams, callback: { reply, error in
if let error = error {
print("Oh no, error: \(error)")
} else {
print("Success: \(reply)")
}
})
// or alternatively you can setup a "self contained" button that listens for press events on itself and invokes an action
var whiskButtonSelfContained = WhiskButton(frame: CGRectMake(0,0,20,20))
whiskButtonSelfContained.listenForPressEvents = true
do {
// use qualified name API which requires do/try/catch
try whiskButtonSelfContained.setupWhiskAction("mypackage/helloConsole", credentials: credentialsConfiguration!, hasResult: false, parameters: nil, urlSession: nil)
whiskButtonSelfContained.actionButtonCallback = { reply, error in
if let error = error {
print("Oh no, error: \(error)")
} else {
print("Success: \(reply)")
}
}
} catch {
print("Error setting up button \(error)")
}