CoatySwift Developer Guide

This document covers everything a Swift developer needs to know about using the CoatySwift framework to implement collaborative IoT applications targeting iOS, iPadOS, and macOS. We assume you know nothing about CoatySwift before reading this guide.

NOTE:

We would like to note that more information about the internals and basics of the Coaty framework can be found in Coaty Communication Protocol. The Coaty JS Developer Guide, even though written for TypeScript, shares many similarities with CoatySwift and we recommend checking out this guide as well if you would like to dig deeper, as it is documented in a more detailed way and provides more extensive features.

Table of Contents

Getting Started

If you want a short, concise look into CoatySwift, feel free to check out the CoatySwift Tutorial with a step-by-step guide on how to set up a basic CoatySwift application. The source code of this tutorial can be found in the Example Xcode folder of the CoatySwift repo. Just clone the repo, run pod install on the Example folder and open the new xcworkspace in Xcode.

You can find additional examples in the swift sections of the coaty-examples repo on GitHub. You will find the following Xcode projects there: Hello World and Remote Operations. They are interoperable with the corresponding Coaty JS examples and intended to be used along with them. These projects can serve as blueprints for how to design CoatySwift applications.

Necessary Background Knowledge

In order to be able to use CoatySwift the way it is intended to, we assume you are familiar with the following programming concepts:

Coaty(Swift) Terminology

TL;DR Terminology

Setup Instructions and Requirements

mDNS Broker Discovery Support

CoatySwift gives you the possibility to discover broker services dynamically via mDNS. You will need an mDNS-supporting broker for this, which you can find here. For the client, add the following lines to your Configuration object:

// - IMPORTANT:
// shouldTryMDNSDiscovery has to bet set to `true`,
// while shouldAutoStart has to be set to `false`.
let mqttClientOptions = MQTTClientOptions(host: brokerIp,
                                          port: UInt16(brokerPort),
                                          enableSSL: enableSSL,
                                          shouldTryMDNSDiscovery: true)

config.communication = CommunicationOptions(mqttClientOptions: mqttClientOptions,
                                            shouldAutoStart: false)

NOTE:

Even if you discover the broker via mDNS, you are still required to specify a fallback host address and corresponding port.

Communication Patterns

Citing the Coaty Protocol Documentation:

The framework uses a minimum set of predefined events and event patterns to discover, distribute, and share object information in a decentralized application:

NOTE:

Although Coaty itself also specifices IoValue and Associate events, these are not included in the CoatySwift versions and therefore are left out of the documentation.

We differentiate between one-way and two-way events. Advertise, Deadvertise and Channel are one-way events. Discover-Resolve, Query-Retrieve, Update-Complete and Call-Return are two-way events.

We also differentiate between publishing events or observing them. When publishing an event, simply put, you send a message over the broker. When observing (or subscribing to) an event, you sign up to receive messages over the broker.

In the following examples, we will show you how you can publish and observe one-way events as well as two-way events.

Publish an Advertise (one-way event)

Note that this procedure is much the same as publishing Deadvertise and Channel events.

// Create the object.
let myExampleObject = CoatyObject(coreType: .CoatyObject,
                                  objectType: "com.mydomain.iot.ExampleObject",
                                  objectId: .init(),
                                  name: "My amazing example object")

// Create an event by using the event factory.
let event = self.eventFactory.AdvertiseEvent.with(object: myExampleObject)

// Publish the event by using the communication manager.
try? self.communicationManager.publishAdvertise(event)

Observe Advertises (one-way event)

Note that this procedure is much the same as observing Deadvertise and Channel events.

try? self.communicationManager
    .observeAdvertise(withCoreType: .Task)
    .subscribe(onNext: { (advertiseEvent) in
        let task = advertiseEvent.data.object as Task

        // Do something with this task...
        print(task.creatorId)

    })
    .disposed(by: disposeBag)

Publish a Discover event and Observe Resolve events (two-way event)

Note that this procedure is much the same as for Query-Retrieve, Update-Complete, and Call-Return events.

let discoverEvent = self.eventFactory.DiscoverEvent
    .with(externalId: "an-external-id")

try? self.communicationManager
    .publishDiscover(discoverEvent)
    .subscribe(onNext: { (resolveEvent) in
        // Do something with your resolve event.
        print(resolveEvent.data.object)

    })
    .disposed(by: disposeBag)

Observe a Discover event (two-way event)

Note that this procedure is much the same as for Query-Retrieve, Update-Complete, and Call-Return events.

try? self.communicationManager
    .observeDiscover()
    .filter { (discoverEvent) -> Bool in
        return discoverEvent.data.isDiscoveringExternalId()
    }
    .subscribe(onNext: { (discoverEvent)
        let externalId = discoverEvent.data.externalId

        // Search for an object with the given external Id...
        let resolveObject = CoatyObject(coreType: .CoreType,
            objectType: "com.mydomain.iot.ResolveExample",
            objectId: .init(),
            name: "My resolve example object")
        resolveObject.externalId = externalId

        let event = self.eventFactory.ResolveEvent.with(object: resolveObject)
        discoverEvent.resolve(resolveEvent: event)

    })
    .disposed(by: disposeBag)

Bootstrapping a Coaty Container

In order to get your Coaty application running, you will have to set up the Coaty container and all its corresponding controllers, as well as data-related classes such as object families. We will provide a step by step explanation of how you can create a container with an exemplary controller.

NOTE:

Unfortunately there is no sequential way (where everything compiles after each step) to set up a container until you added all of the required components. It is probably the easiest to add an empty ObjectFamily and at least one Controller before you start to resolve your Container. We suggest taking a look at the Hello World example to see how the final result looks like.

We suggest setting up the main structure of the container as part of the AppDelegate.swift.

  1. Make sure to import CoatySwift in the top.
import CoatySwift
  1. Create a global variable coatyContainer. This will hold a reference to our Coaty container. It is needed because otherwise all of our references go out of scope. For example, in this case, the MQTT connection is simply terminated.
// ...
import CoatySwift

/// Save a reference of your container in the app delegate to
/// make sure it stays alive during the entire life-time of the app.
var coatyContainer: Container<ExampleObjectFamily>?

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
  1. We will now go on to instantiate our controllers. Here, we assume that you have already created an ExampleController as well as an ExampleObjectFamily. Feel free to read up on controllers and object families in this section. The key note of this step is to indicate which key maps to which controller, in order to be able to access these controllers later after the container has been bootstrapped.
// Here, you define which controllers you want to use for your application.
// Note that the keys (such as "ExampleController")
// do NOT have to have the exact name of their controller class. Feel free to give
// them any names you want. The _mapping_ is the important thing, so which name
// maps to what controller class.
let components = Components(controllers: [
    "ExampleController": ExampleController<ExampleObjectFamily>.self
])
  1. The next step is to specify a configuration for your container. Below we have added an example configuration which should be appropriate for most Coaty beginner projects. Note the MQTTClientOptions in particular: Here, you pass in your broker address, port and SSL option.
/// This method creates an exemplary Coaty configuration.
/// You can use it as a basis for your application.
private func createExampleConfiguration() -> Configuration? {
    return try? .build { config in

        // This part defines common options shared by all container components,
        // including e.g. an associated user or associated device.
        config.common = CommonOptions()

        // Adjusts the logging level of CoatySwift messages, which is especially
        // helpful when you want to debug applications.
        config.common?.logLevel = .info

        // You can also add extra information to your configuration in the form of a
        // [String: String] dictionary.
        config.common?.extra = ["ContainerVersion": "0.0.1"]

        // Define the communication-related options, such as the IP address of your broker and
        // the port it exposes. Also, make sure to immediately connect with the broker,
        // indicated by `shouldAutoStart: true`.
        let mqttClientOptions = MQTTClientOptions(host: brokerIp,
                                          port: UInt16(brokerPort))
        config.communication = CommunicationOptions(mqttClientOptions: mqttClientOptions,
                                                    shouldAutoStart: true)

        // The communicationManager will advertise its identity upon connection to the
        // MQTT broker.
        config.communication?.shouldAdvertiseIdentity = true
    }
}

//...

/// And then, simply call it when you need it in order to integrate it
/// into your container configuration.
guard let configuration = createExampleConfiguration() else {
    print("Invalid configuration! Please check your options.")
    return
}

//...
  1. Lastly, the only thing you need to do is to resolve everything and assign the variable we previously defined, namely, coatyContainer, with the return value of container.resolve(…). Below code shows the last step in boostrapping a Coaty container:
// Pass in the previously defined components, configuration and, most importantly, the
// ObjectFamily (in this case, `ExampleObjectFamily`). Then, call Container.resolve(...),
// save it into our global variable, and you're done!
coatyContainer = Container.resolve(components: components,
                                   configuration: configuration,
                                   objectFamily: ExampleObjectFamily.self)

TL;DR Container Bootstrapping

  1. Create a global variable that holds a reference to the coatyContainer.
  2. Specify all controllers that you want to use.
  3. Create an appropriate configuration.
  4. Simply call container.resolve(…) and assign its return value to the coatyContainer global variable from step 1.

Creating Controllers

As previously mentioned, Coaty controllers are the components encapsulating business logic in your application. Below we will show you how you can build your own Coaty container and add networking functionality in order to communicate with other Coaty agents.

Each controller provides lifecycle methods that are called by the framework, shown here:

class ExampleController<Family: ObjectFamily>: Controller<Family> {

    override func onInit() {
        // Perform additional setup.
    }

    override func onContainerResolved(container: Container<Family>) {
        // Gives you a reference to the container as soon as all components have been resolved.
        // Useful if you need to hold a reference to another controller.
    }

    override func onCommunicationManagerStarting() {
        // Here you can setup your subscriptions or start publishing some events.
    }

    override func onCommunicationManagerStopping() {
        // Teardown of subscriptions when the communication manager goes offline.
    }

    override func onDispose() {
        // Teardown when the container is disposed.
    }
}

Of course you can also additional functionality, such as new methods or variables, references, and so on. A very simplified ExampleViewController could look like this:

class ExampleController<Family: ObjectFamily>: Controller<Family> {

    override func onCommunicationManagerStarting() {
        print("[ExampleController] - onCommunicationManagerStarting()")
    }
}

In the next steps, you could add publish and subscribe handler, as previously mentioned in this section.

Object Family and Custom Data Types

To use custom objects or datatypes with Coaty you may extend the preexisting core types. Use standard Swift classes that extend the base implementation, e.g. define your custom object that inherits from CoatyObject as in the following example:

// ...

final class ExampleObject: CoatyObject {

    let myValue: String

    init(myValue: String) {
        self.myValue = myValue
        super.init(coreType: .CoatyObject,
                   objectType: ExampleObjectFamily.exampleObject.rawValue,
                   objectId: .init(),
                   name: "ExampleObject Name :)")
    }

    // MARK: Codable methods.

    enum CodingKeys: String, CodingKey {
        case myValue
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        myValue = try container.decode(String.self, forKey: .myValue)
        try super.init(from: decoder)
    }

    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        let container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(myValue, forKey: .myValue)
    }

}

NOTE:

You need to implement the conformance to the Codable protocol for your custom objects. Also, make sure to call the super implementations for the init(from:)initializer as well as the encode(to:) method.

If you need to encode / decode a property value of a custom Coaty object type that can be any valid JSON data, and you don’t know the JSON structure in advance, declare the property type as AnyCodable. Using this type, you can encode or decode mixed-type values in dictionaries and other collections that require Encodable or Decodable conformance.

To let the framework know about the new custom type you need to define a so called ObjectFamily. The ObjectFamily provides a mapping from a type name that is encoded by the objectType attribute of each CoatyObject to a Swift class that is used to instantiate an actual Swift object from the JSON representation that is being sent over the wire. A minimal ObjectFamily for the ExampleObject that was defined above looks like this:

enum ExampleObjectFamily: String, ObjectFamily {
    case exampleObject = "io.coaty.hello-coaty.example-object"

    func getType() -> AnyObject.Type {
        switch self {
        case .exampleObject:
            return ExampleObject.self
        }
    }
}

The ObjectFamily is one of the parameters that are required for the configuration of your CoatySwift application. After setting an ObjectFamily during the bootstrapping of a container you will have full type safety and do not need to worry about manual unmarshalling.

If you wish to see a more elaborate example for an ObjectFamily, please see the base CoatyObjectFamily in the CoatySwift framework.

TL;DR Custom Data Types

  1. Create a new Swift object that inherits from CoatyObject or another core type.
  2. Implement conformance to the Codable protocol. Make sure to call the super implementations of the initializer and the encode method.
  3. Create one ObjectFamily per CoatySwift application. Add your object type to the enum and assign the correct Swift type.

FAQ

Interacting with CoatyControllers

A best practice to pass information from CoatyControllers to UIViewControllers or other application components can be achieved by implementing the delegate pattern. Assuming you configured your container and made it available globally in your application via a variable named coatyContainer you can load a controller to set a delegate as follows:

import CoatySwift

class ViewController: UIViewController {

    private var controller: ExampleController<ExampleObjectFamily>?

    override func viewDidLoad() {
        super.viewDidLoad()

        self.controller = coatyContainer?.getController(name: "ExampleController")

        // Set the ViewController as the delegate.
        self.controller?.delegate = self

        // Call methods of the controller.
        self.controller?.publishExample()
    }

    // ...
}

Managing Subscriptions

There are two ways how to manage subscriptions:

  1. You can start and stop the Communication Manager manually by calling communicationManager.startClient() and communicationManager.endClient(). Remember that your previous subscriptions will become inactive if you call communicationManager.endClient(). It is therefore recommended to set up all the subscriptions anew in the .onCommunicationManagerStarting() methods in each controller.

  2. Subscriptions can be disposed manually as well. You can do this by manually calling .dispose() on an active subscription.

Terminology

It is important to remember that a CoatySwift Controller has nothing to do with the regular UIViewController, and likewise, the container variable used in the decode and encode methods is not related in any way to the Coaty Container object.

Component vs. Identity

Please note that in CoatySwift, the Component CoatyObject from Coaty JS had to be renamed to Identity. This is because Component causes a name conflict in the macOS runtime. However, regardless of the different name, CoatySwift and Coaty JS are compatible.

Additional resources

We would like to point you to additional resources if you want to dig deeper into CoatySwift and Coaty itself.

The following resources are part of Coaty JS, but the CoatySwift API aims to be as close as possible to the reference implementation. Therefore, we suggest checking out these resources as well: