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.
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
CoatySwiftExample
Xcode folder of the CoatySwift repo. Just clone the repo, and open the
Example/Example.xcodeproj
using 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.
In order to be able to use CoatySwift the way it is intended to, we assume you are familiar with the following programming concepts:
TL;DR
- Every iOS/iPadOS/macOS app hosts one or more Coaty agents (usually one)
- Every Coaty agent holds one container
- Every container has
- 1…n controllers
- 1 configuration
- 1 communication manager
In Coaty, every application component that communicates with other application components by use of Coaty communication flows is called an agent. So simply speaking, we consider an iOS or a macOS application to host (at least) one agent.
Every agent holds a container. A container basically defines entry and exit points for a Coaty agent and provides lifecycle management for its controllers.
Every container has 1…n controllers. A controller encapsulates communication business logic, and most importantly, all access methods related to any form of communication flow with other agents that you will be using in your application will be called from inside a controller. Each controller should encapsulate a single dedicated functionality. These controllers are in no way related to Apple’s UIViewControllers!
Every container holds exactly one communication manager. A communication manager lets you publish and subscribe to communication events, basically handling all types of communication flow between Coaty agents.
Every container has a configuration: Defines options for the container, as
well as the controllers. There are many options available. You can check out
example configs in the sections below, or the configs found in the CoatySwiftExample
folder in the CoatySwift Xcode project.
To build and run Coaty agents with the CoatySwift technology stack you need XCode 10.2 or higher.
Integrate CoatySwift in your project: CoatySwift is available through both CocoaPods and Swift Package Manager.
1.8.4
of
CocoaPods, i.e. running pod --version
should yield 1.8.4
or higher.You can add the CoatySwift pod to the Podfile of your app as follows:
target 'MyApp' do
pod 'CoatySwift', '~> 2.4.0'
end
Then run a pod install
for a new installation or a pod update CoatySwift
to update the pod to the specified version.
dependencies
attribute in your Package.swift
file.dependencies: [
.package(url: "https://github.com/coatyio/coaty-swift", from: "2.4.0"),
]
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:
let mqttClientOptions = MQTTClientOptions(shouldTryMDNSDiscovery: true)
config.communication = CommunicationOptions(mqttClientOptions: mqttClientOptions)
NOTE: Broker host and port settings and the
shouldAutoStart
option are ignored if mDNS broker discovery is enabled.
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:
Advertise an object: Multicast an object to parties interested in objects of a specific core or object type.
Deadvertise an object by its unique ID: Notify subscribers when capability is no longer available; for abnormal disconnection of a party, last will concept can be implemented by sending this event.
Channel: Multicast objects to parties interested in any kind of objects delivered through a channel with a specific channel identifier.
Discover - Resolve: Discover an object and/or related objects by external ID, internal ID, or object type, and receive responses by Resolve events.
Query - Retrieve: Query objects by specifying selection and ordering criteria, receive responses by Retrieve events.
Update - Complete: Request or suggest an object update and receive accomplishments by Complete events.
Call - Return: Request execution of a remote operation and receive results by Return events.
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 (i.e. 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.
Note that this procedure is much the same as publishing Deadvertise and Channel events.
// Create a Task object.
let myTaskObject = Task(creatorId: .init(),
creationTimestamp: .nowMillis(),
status: .request,
name: "MyTask")
// Create the event.
let event = try! AdvertiseEvent.with(object: myTaskObject)
// Publish the event by the communication manager.
self.communicationManager.publishAdvertise(event)
Note that this procedure is much the same as observing Deadvertise and Channel events.
self.communicationManager
.observeAdvertise(withCoreType: .Task)
.subscribe(onNext: { (advertiseEvent) in
let task = advertiseEvent.data.object as! Task
// Do something with this task...
print(task.name)
})
.disposed(by: self.disposeBag)
Note that this procedure is much the same as for Query-Retrieve, Update-Complete, and Call-Return events.
let discoverEvent = DiscoverEvent.with(externalId: "an-external-id")
self.communicationManager
.publishDiscover(discoverEvent)
.subscribe(onNext: { (resolveEvent) in
// Do something with your Resolve event.
print(resolveEvent.data.object)
})
.disposed(by: self.disposeBag)
Note that this procedure is much the same as for Query-Retrieve, Update-Complete, and Call-Return events.
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 resolvedObject = CoatyObject(coreType: .CoatyObject,
objectType: "com.mydomain.ExampleObject",
objectId: .init(),
name: "My resolved example object")
resolvedObject.externalId = externalId
let event = ResolveEvent.with(object: resolvedObject)
discoverEvent.resolve(resolveEvent: event)
})
.disposed(by: self.disposeBag)
Note: Please refer to coaty-js Developer Guide for general informations (concepts, constraints and communication event flow) regarding IO Routing.
IO router classes and controller for IO sources/IO actors are provided in the IORouting directory of CoatySwift.
The following example defines a temperature measurement routing scenario with three temperature sensor sources (each with a different strategy for publishing values) and two actors with compatible data value types and formats. The IO context for this scenario defines an operating state, either normal or emergency. In each state, exactly one of the two actors should consume IO values emitted by the sources.
Note: This example is fully implemented in Coaty example on IO Routing.
// At first define a class which will represent the context for IO Routing
// This class extends IoContext class with an additional property operatingState
// Make sure to follow all the steps from section ´Custom object types´
// (in this Developer Guide) while subclassing IoContext
class TemperatureIoContext: IoContext {
var operatingState: String
override class var objectType: String {
return register(objectType: "coaty.TemperatureIoContext", with: self)
}
init(coreType: CoreType, objectType: String, objectId: CoatyUUID, name: String, operatingState: String) {
self.operatingState = operatingState
super.init(coreType: coreType, objectType: objectType, objectId: objectId, name: name)
}
enum CodingKeys: String, CodingKey {
case operatingState
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.operatingState = try container.decode(String.self, forKey: .operatingState)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(operatingState, forKey: .operatingState)
}
}
// Common context for IO routing
let ioContext = TemperatureIoContext(coreType: .IoContext,
objectType: "coaty.TemperatureIoContext",
objectId: CoatyUUID(uuidString: "b61740a6-95d7-4d1a-8be5-53f3aa1e0b79")!,
name: "TemperatureMeasurement",
operatingState: "normal")
// Initialize IoSource for .None Strategy.
// This strategy simply publishes all values as they are being sent.
let source1 = IoSource(valueType: "coaty.test.Temperature[Celsius]",
updateStrategy: .None,
name: "Temperature Source 1",
objectType: CoreType.IoSource.rawValue,
objectId: CoatyUUID(uuidString: "c547e5cd-ef99-4ccd-b109-fc472fc2d421")!)
// Initialize IoSource for .Sample Strategy.
// This strategy means: Publish the most recent values within periodic time intervals
// according to the recommended update rate assigned to the IO source. More information in documentation.
let source2 = IoSource(valueType: "coaty.test.Temperature[Celsius]",
updateStrategy: .Sample,
updateRate: 5000,
name: "Temperature Source 2",
objectType: CoreType.IoSource.rawValue,
objectId: CoatyUUID(uuidString: "2e9949f7-a8ef-435b-88a9-527c0a9414c3")!)
// Initialize IoSource for .Throttle Strategy.
// This strategy means: Only publish a value if a particular timespan has
// passed without it publishing another value. More information in documentation.
let source3 = IoSource(valueType: "coaty.test.Temperature[Celsius]",
updateStrategy: .Throttle,
updateRate: 5000,
name: "Temperature Source 3",
objectType: CoreType.IoSource.rawValue,
objectId: CoatyUUID(uuidString: "200cc37b-df20-4425-a16f-5c0b42d04dbb")!)
// Configuration of agent1 with an IoNode for three io sources in common options
let ioNodeDefinition = IoNodeDefinition(ioSources: [source1, source2, source3],
ioActors: nil,
characteristics: nil)
let commonOptions = CommonOptions(ioContextNodes: ["TemperatureMeasurement" : ioNodeDefinition],
logLevel: .info)
let actor1 = IoActor(valueType: "coaty.test.Temperature[Celsius]",
updateRate: 5000,
name: "Temperature Actor 1",
objectType: CoreType.IoActor.rawValue,
objectId: CoatyUUID(uuidString: "a731fc40-c0f8-486f-b5b6-b653c3cabaea")!)
// Configuration of agent 2 with an IoNode for actor 1 in common options
let ioNodeDefinition = IoNodeDefinition(ioSources: nil,
ioActors: [actor1],
characteristics: nil)
let commonOptions = CommonOptions(ioContextNodes: ["TemperatureMeasurement" : ioNodeDefinition],
logLevel: .info)
// Temperature Actor 2 (Emergency operating state).
let actor2 = IoActor(valueType: "coaty.test.Temperature[Celsius]",
updateRate: 5000,
name: "Temperature Actor 2",
objectType: CoreType.IoActor.rawValue,
objectId: CoatyUUID(uuidString: "a60a74f3-3d26-446f-a358-911867544944")!)
// Configuration of agent 3 with an IoNode for actor2 in common options
let ioNodeDefinition = IoNodeDefinition(ioSources: nil,
ioActors: [actor1],
characteristics: nil)
let commonOptions = CommonOptions(ioContextNodes: ["TemperatureMeasurement" : ioNodeDefinition],
logLevel: .info)
Use the RuleBasedIoRouter controller class to realize rule-based routing of data from IO sources to IO actors. By defining application-specific routing rules you can associate IO sources with IO actors based on arbitrary application context.
// Configure the rules used by the RuleBasedIoRouter.
let condition1: IoRoutingRuleConditionFunc = { (source, sourceNode, actor, actorNode, context, router) -> Bool in
guard let operatingStateResponsibility = actorNode.characteristics?["isResponsibleForOperatingState"] as? String,
let context = context as? TemperatureIoContext else {
return false
}
return operatingStateResponsibility == "normal" && context.operatingState == "normal"
}
let condition2: IoRoutingRuleConditionFunc = { (source, sourceNode, actor, actorNode, context, router) -> Bool in
guard let operatingStateResponsibility = actorNode.characteristics?["isResponsibleForOperatingState"] as? String,
let context = context as? TemperatureIoContext else {
return false
}
return operatingStateResponsibility == "emergency" && context.operatingState == "emergency"
}
let rules: [IoAssociationRule] = [
IoAssociationRule(name: "Route temperature sources to normal actors if operating state is normal",
valueType: "coaty.test.Temperature[Celsius]",
condition: condition1),
IoAssociationRule(name: "Route temperature sources to emergency actors if operating state is emergency",
valueType: "coaty.test.Temperature[Celsius]",
condition: condition2)
]
// Configure the required options for a RuleBasedIoRouter
let routerOptions = ControllerOptions(extra: ["ioContext" : ioContext, "rules": rules])
// Controller options are always mapped by the controller class name as String.
// This variable is later used in the construction of the Configuration object.
let controllers = ControllerConfig(controllerOptions: ["RuleBasedIoRouter": routerOptions])
An IO router makes its IO context available by advertisement and for discovery (by core type, object type or object Id) and listens for Update-Complete events on its IO context. To trigger reevaluation of association rules by an IO router, simply publish an Update event for the discovered IO context object.
var ioContext: TemperatureIoContext
// Discover temperature measurement context from IO router
coatyContainer?
.communicationManager?
.publishDiscover(DiscoverEvent.with(objectTypes: ["coaty.test.TemperatureIoContext"])).subscribe(onNext: { resolve in
self.ioContext = resolve.data.object as! TemperatureIoContext
})
// Change context operating state to trigger rerouting from sources to emergency actors
self.ioContext.operatingState = "normal"
coatyContainer?.communicationManager
.publishUpdate(UpdateEvent.with(object: ioContext)).subscribe(onNext: { complete
// Updated object is returned.
self.ioContext = complete.data.object as! TemperatureIoContext
})
The Communication Manager supports methods to control IO routing in your agent: Use publishIoValue to send IO value data for an IO source. Use observeIoState and observeIoValue to receive IO state changes and IO values for an IO actor.
To further simplify management of IO sources and IO actors, the framework provides specific controller classes on top of these methods:
IoSourceController: Provides data transfer rate controlled publishing of IO values for IO sources and monitoring of changes in the association state of IO sources. This controller respects the backpressure strategy of an IO source in order to cope with IO values that are more rapidly produced than specified in the recommended update rate.
IoActorController: Provides convenience methods for observing IO values and for monitoring changes in the association state of specific IO actors. Note that this controller class caches the latest IO value received for the given IO actor (using BehaviorSubjects). When subscribed, the current value (or nil if none exists yet) is emitted immediately. Due to this behavior the cached value of the observable will also be emitted after reassociation. If this is not desired use self.communicationManager.observeIoValue instead. This method doesn’t cache any previously emitted value.
Take a look at these controllers in action in the CoatySwift Example on IO Routing
If you want to learn how to use Sensor Things API implementation in Coaty Swift please refer to Coaty Swift Sensor Things Guide.
The Coaty framework provides the object type Log
for decentralized structured
logging of any kind of informational events in your Coaty agents, such as
errors, warnings, system and application-specific messages. Log objects are
usually published to interested parties using an Advertise event. These log
objects can then be collected and ingested into external processing pipelines
such as the ELK Stack.
A controller can publish a log object by creating and advertising a Log
object. You can specify the level of logging (debug, info, warning, error,
fatal), the message to log, its creation timestamp, and other optional
information about the host environment in which this log object is created. You
can also extend the Log
object with custom property-value pairs.
You can also specify log tags as an array of string values in the Log.logTags
property. Tags are used to categorize or filter log output. Agents may introduce
specific tags, such as “service” or “app”, usually defined at design time.
You can also specify log labels as a set of key-value label pairs in the
logLabels
property. It can be used to add context-specific information to a
log object. For example, labels are useful in providing multi-dimensional data
along a log entry to be exploited by external logging services, such as
Prometheus.
For convenience, the base Controller
class provides methods for
publishing/advertising log objects:
logDebug
logInfo
logWarning
logError
logFatal
The base Controller
class also defines a protected method
extendLogObject(log: Log)
which is invoked by the controller whenever one of
the above log methods is called. The controller first creates a Log
object
with appropriate property values and passes it to this method before advertising
it. You can overwrite this method to additionally set certain properties (such
as Log.hostname
or Log.logLabels
). For example, a Node.js agent could add
the hostname and other host characteristics to the Log
object like this:
override func extendLogObject(log: Log) {
log.logHost.hostname = hostname;
log.logLabels = [
"operatingState": self.communicationManager.operatingState
]
}
To collect all log objects advertised by agent controllers, implement a logging
controller that observes Advertise events on the core type Log
. Take a look at
the Hello World example of the Coaty framework to see how this is implemented in
detail.
Future versions of the framework could include predefined logging controllers
that collect Log
entries, store them persistently; output them to file or
console, and provide a query interface for analyzing and visualizing log entries
by external tools.
In order to get your Coaty application running, you will have to set up the Coaty container and its controllers. We will provide a step by step explanation of how you can create a container with an exemplary controller.
TL;DR
- Create a global variable that holds a reference to the
coatyContainer
.- Register all controllers and object types that you want to use.
- Create an appropriate container configuration.
- Simply call
container.resolve(…)
and assign its return value to thecoatyContainer
global variable from step 1.
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 at least one
Controller
before you start to resolve yourContainer
. We suggest taking a look at theHello World
example or the example that is integrated in CoatySwift to see how the final result looks like.
We suggest setting up the main structure of the container as part of the
AppDelegate.swift
.
Make sure to import CoatySwift in the top.
import CoatySwift
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 and communication is terminated.
// ...
import CoatySwift
/// Save a reference of your container in the app delegate to
/// make sure it stays alive during the entire lifetime of the app.
var coatyContainer: Container?
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
We will now go on to register our custom controllers and object types. Here,
we assume that you have already defined an ExampleController
(as explained
here) as well as an ExampleObject
(as explained
here). 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 specify which Coaty controllers and object types you want to use in
// your application.
//
// Note that the controller keys (such as "ExampleController")
// do NOT have to have the exact name of their controller class. Feel free to give
// them any unique names you want. The _mapping_ is the important thing, so which name
// maps to what controller class.
let components = Components(controllers: [
"ExampleController": ExampleController.self
],
objectTypes: [
ExampleObject.self,
])
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’s host address and port (and other optional connection options).
/// 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 if you want to test or debug applications (default is .error).
config.common?.logLevel = .info
// Configure an expressive `name` of the container's identity here.
config.common?.agentIdentity = ["name": "Example Agent"]
// 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 communication-related options, such as the host address of your broker
// (default is "localhost") and the port it exposes (default is 1883). Define a
// unqiue communication namespace for your application and make sure to immediately
// connect with the broker, indicated by `shouldAutoStart: true`.
let mqttClientOptions = MQTTClientOptions(host: brokerHost,
port: UInt16(brokerPort))
config.communication = CommunicationOptions(namespace: "com.example",
mqttClientOptions: mqttClientOptions,
shouldAutoStart: 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
}
//...
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
bootstrapping a Coaty container:
// Pass in the previously defined components and configuration.
// Then, call Container.resolve(...), save it into our global variable,
// and you're done!
coatyContainer = Container.resolve(components: components,
configuration: configuration)
As previously mentioned, Coaty controllers are the components encapsulating communication business logic in your application.
Each controller provides lifecycle methods that are called by the framework, shown here:
class ExampleController: Controller {
// MARK: - Controller lifecycle methods.
override func onInit() {
// Perform initial setup.
// Access the container by `self.container`.
// Access other controllers by `self.container.getController(name:)`
}
override func onCommunicationManagerStarting() {
super.onCommunicationManagerStarting()
// Setup your observations or start publishing events.
}
override func onCommunicationManagerStopping() {
super.onCommunicationManagerStopping()
// Perform side effects when communication manager is stopped.
}
override func onDispose() {
// Teardown resources when the container is disposed.
}
}
Of course you can also additional functionality, such as new methods or variables, references, and so on. A very basic ExampleController could look like this:
class ExampleController: Controller {
override func onCommunicationManagerStarting() {
super.onCommunicationManagerStarting()
print("[ExampleController] - onCommunicationManagerStarting()")
}
}
In the next steps, you should add publish and subscribe handlers, as previously explained in this section.
TL;DR
- Create a new Swift class that inherits from
CoatyObject
, any other Coaty core type, or another custom object type.- Register the new class for your custom object type with CoatySwift and use this custom object type in the initializer.
- Implement conformance to the Codable protocol. Make sure to call the super implementations of the initializer and the encode method.
Coaty comes with an opinionated set of core object types to be used or extended
by CoatySwift applications. These Coaty objects are the subject of communication
between Coaty agents. Core types include the base CoatyObject
, and others,
such as Task
or User
. For a detailed specification of the Coaty object model
see
here.
To define custom, i.e. application-specific object types use standard Swift
classes that extend predefined core types or other custom types. For example,
define your custom type that inherits from CoatyObject
as in the following
example:
import Foundation
import CoatySwift
final class ExampleObject: CoatyObject {
// MARK: - Class registration.
override class var objectType: String {
return register(objectType: "hello.coaty.ExampleObject", with: self)
}
// MARK: - Properties.
let myValue: String
// MARK: Initializers.
init(myValue: String) {
self.myValue = myValue
super.init(coreType: .CoatyObject,
objectType: ExampleObject.objectType,
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)
}
}
Ensure that the class for your custom Coaty object type is registered with CoatySwift. This involves two separate registration steps:
objectType
.Components
as explained
previously.Note that the registered object type is also useful when observing objects of this object type, like this:
try! self.communicationManager
.observeAdvertise(withObjectType: ExampleObject.objectType)
.subscribe(onNext: { (event) in
The second registration step is necessary because Swift only executes class
variable initializers lazily on first usage. It guarantees that the class
registration performed by ExampleObject.objectType
has been completed before
the first corresponding object is received over the wire and decoded.
The objectType
parameter specified in the super.init()
initializer must
equal the registered object type.
You need to implement conformance to the
Codable protocol for
your custom object types. Also, make sure to call the super implementations for
the init(from:)
initializer as well as the encode(to:)
method.
If you need to decode or encode 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
decode or encode mixed-type values in dictionaries and other collections that
require Decodable
or Encodable
conformance. For decoding, simply cast the
value
property of the AnyCodable
to the expected Swift type.
Likewise, to decode a custom property value that is of any (also variable) Coaty
object type or a collection thereof, use AnyCoatyObjectDecodable
in the
container.decode(_ type:)
method. For an example, see the Snapshot
core type
class which decodes any CoatyObject in its object
property.
Note
When decoding an object type that has not been registered, an instance of the core type class is created with all core type properties filled in. Any other fields present on the decodable object are added to the
custom
dictionary property of the created instance.This approach is especially useful if you want to observe Coaty objects of arbitrary object types for which no Swift class definitions are defined and registered in your app.
A best practice to pass information from a Coaty Controller
to a
UIViewController
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?
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?.advertiseExample()
}
// ...
}
There are several ways how to manage subscriptions of Observables returned by
communication manager’s observe...()
event methods and publish...()
two-way
event methods:
.disposed(by: self.disposeBag)
after the subscribe()
method call. Remember that these subscriptions will become disposed if you
call communicationManager.stop()
. It is therefore recommended to set up all
these subscriptions anew in the controller’s
.onCommunicationManagerStarting()
method which is invoked when calling
communicationManager.start()
.take
or takeUntil
. But if completion
doesn’t happen before the communication manager is stopped, the observable
still needs to be disposed as recommended above..dispose()
on an active subscription in case you are sure to no longer need
it.The communication manager provides two methods to observe operating state
changes via getOperatingState()
and communication state changes via
getCommunicationState()
.
Operating states indicate whether the communication manager is currently started
(started
) or stopped (stopped
). Communication states indicate the
connectivity state (offline
or online
).
When the communication manager is started, it tries to connect to the underlying communication infrastructure. Unless stopped, it automatically tries to reconnect periodically whenever the connection is lost.
When the communication manager is stopped, it permanently disconnects from the
underlying communication infrastructure. Afterwards, communication events are no
longer dispatched and emitted. You can start the Communication Manager again
later using the start()
method.
The communication manager is started by
start()
method explicitely,shouldAutoStart
option as true
(opt-in),shouldTryMDNSDiscovery
option as true
(opt-in).The communication manager is stopped by
stop()
method explicitely,Container.shutdown()
method.Starting and stopping actions trigger corresponding state changes on the
operating state Observable. Connections and disconnections trigger corresponding
state changes on the communication state Observable. Note that communication
state changes might happen while the communication manager is in started
state. In stopped
operating state, the communication state is always
offline
.
Note that operating state changes also trigger invocation of the Controller
lifecycle methods onCommunicationManagerStarting
or
onCommunicationManagerStopping
.
Execution of publish...
and observe...
event methods by the communication
manager is deferred, if it is either stopped or started but offline, i.e. not
connected currently.
All your subscriptions issued while the communication manager is stopped or offline will be (re)applied when it (re)connects again. Publications issued while the Communication Manager is stopped or offline will be applied only after the next (re)connect. Publications issued in online state will not be deferred, i.e. not reapplied after a reconnect.
If you stop the communication manager by executing its stop
method, all
deferred publications and subscriptions will be discarded.
For testing and debugging, you can output the external representation of a Coaty object instance as follows:
let task = Task(...)
print(task.json)
For testing and debugging, you can output the external representation of an
AnyCodable
as follows:
let parameters: [String: AnyCodable] = ["on": .init(true),
"color": .init([255, 140, 0, 1]]),
"luminosity": .init(0.75),
"switchTime": .init(10)]
print(PayloadCoder.encode(parameters))
To realize distributed lifecycle management for Coaty agents, the agent container is assigned a unique identity object to be accessible by all controllers and the communication manager.
Whenever the communication manager is started, it advertises the agent’s identity and makes it discoverable. Whenever the communication manager is stopped (normally or abnormally), its agent’s identity is deadvertised. This way, other agents can track the agent’s lifecycle.
By design, there is no echo suppression of communication events. The communication manager dispatches any incoming event to every controller that observes it, even if the controller published the event itself.
If echo suppression of communication events is required for your custom controller, place it into its own container and filter out observed events whose event source ID equals the object ID of the container’s identity, like this:
self.communicationManager
.observeDiscover()
.filter { (discoverEvent) -> Bool in
return discoverEvent.sourceId != self.container.identity.objectId
}
.subscribe(onNext: { (discoverEvent)
// Handle non-echo events only.
})
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.
We would like to point you to additional resources if you want to dig deeper into CoatySwift and Coaty itself.
swift
sections of the coaty-examples repo on
GitHub.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: