siegesmund / SwiftDDP

A Meteor client, written in Swift
MIT License
145 stars 60 forks source link

SwiftDDP

A client for Meteor servers, written in Swift

version 0.4.1

License

MIT

Version License Platform

Installation

With CocoaPods. Add the following line to your Podfile:

pod "SwiftDDP", "~> 0.4.1"

With Swift Package Manager. Add the following line to the dependencies in your Package.swift:

        .package(url: "https://github.com/y-ich/SwiftDDP.git", .branch("master")),

Quick Start

Setting basic configuration options

import SwiftDDP

Meteor.client.allowSelfSignedSSL = true     // Connect to a server that uses a self signed ssl certificate
Meteor.client.logLevel = .Info              // Options are: .Verbose, .Debug, .Info, .Warning, .Error, .Severe, .None

Connecting to a Meteor server


// Meteor.connect will automatically connect and will sign in using
// a stored login token if the client was previously signed in.

Meteor.connect("wss://todos.meteor.com/websocket") {
    // do something after the client connects
}

Login & Logout with password

Login using email and password.

Meteor.loginWithPassword("user@swiftddp.com", password: "********") { result, error in
    // do something after login
}

Login using username and password.

Meteor.loginWithUsername("swiftddp", password: "********") { result, error in
    // do something after login
}

Log out.

Meteor.logout() { result, error in
    // do something after logout
}

The client also posts a notification when the user signs in and signs out, and during connection failure events.

// Notification name (a string global variable)
DDP_USER_DID_LOGIN
DDP_USER_DID_LOGOUT
//Websocket/DDP connection failure events
DDP_WEBSOCKET_CLOSE
DDP_WEBSOCKET_ERROR
DDP_DISCONNECTED
DDP_FAILED

// Example
NSNotificationCenter.defaultCenter().addObserver(self, selector: "userDidLogin", name: DDP_USER_DID_LOGIN, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "userDidLogout", name: DDP_USER_DID_LOGOUT, object: nil)

func userDidLogin() {
    print("The user just signed in!")
}

func userDidLogout() {
    print("The user just signed out!")
}

Subscribe to a subset of a collection

Meteor.subscribe("todos")

Meteor.subscribe("todos") {
    // Do something when the todos subscription is ready
}

Meteor.subscribe("todos", [1,2,3,4]) {
    // Do something when the todos subscription is ready
}

Change the subscription's parameters and manage your subscription with unsubscribe


// Suppose you want to subscribe to a list of all cities and towns near a specific major city

// Subscribe to cities near Boston
let id1 = Meteor.subscribe("cities", ["lat": 42.358056 ,"lon": -71.063611]) {
    // You are now subscribed to cities associated with the coordinates 42.358056, -71.063611
    // id1 contains a key that allows you to cancel the subscription associated with 
    // the parameters ["lat": 42.358056 ,"lon": -71.063611]
}

// Subscribe to cities near Paris
let id2 = Meteor.subscribe("cities", ["lat": 48.8567, "lon": 2.3508]){
    // You are now subscribed to cities associated with the coordinates 48.8567, 2.3508
    // id2 contains a key that allows you to cancel the subscription associated with 
    // the parameters ["lat": 48.8567 ,"lon": 2.3508]
}

// Subscribe to cities near New York
let id3 = Meteor.subscribe("cities", ["lat": 40.7127, "lon": -74.0059]){
    // You are now subscribed to cities associated with the coordinates 40.7127, -74.0059
    // id3 contains a key that allows you to cancel the subscription associated with 
    // the parameters ["lat": 40.7127 ,"lon": -74.0059]
}

// When these subscriptions have completed, the collection associated with "cities" will now contain all
// documents returned from the three subscriptions

Meteor.unsubscribe(withId: id2) 
// Your collection will now contain cities near Boston and New York, but not Paris
Meteor.unsubscribe("cities")    
// You are now unsubscribed to all subscriptions associated with the publication "cities"

Call a method on the server

Meteor.call("foo", [1, 2, 3, 4]) { result, error in
    // Do something with the method result
}

When passing parameters to a server method, the parameters object must be serializable with NSJSONSerialization

Simple in-memory persistence

SwiftDDP includes a class called MeteorCollection that provides simple, ephemeral dictionary backed persistence. MeteorCollection stores objects subclassed from MeteorDocument. Creating a collection is as simple as:

class List: MeteorDocument {

    var collection:String = "lists"
    var name:String?
    var userId:String?

}

let lists = MeteorCollection<List>(name: "lists")   // As with Meteorjs, the name is the name of the server-side collection  
Meteor.subscribe("lists")

For client side insertions, updates and removals:

let list = List(id: Meteor.client.getId(), fields: ["name": "foo"])

// Insert the object on both the client and server.
lists.insert(list)

// Update the object on both the client and server
list.name = "bar"
lists.update(list)

// Remove the object on both the client and server
lists.remove(list)

For each operation the action is executed on the client, and rolled back if the server returns an error.

Tips for CLI

If you make a command line tool, you need to call the function dispatchMain in main thread after proper DDP settings. Otherwise, you will encounter a dead lock.


Meteor.connect("wss://todos.meteor.com/websocket") {
    // do something after the client connects
}
dispatchMain()

Example: Creating an array based custom collection

The following pattern can be used to create custom collections backed by any datastore

In this example, we'll create a simple collection to hold a list of contacts. The first thing we'll do is create an object to represent a contact. This object has four properties and a method named update that maps the fields NSDictionary to the struct's properties. Update is called when an object is created and when an update is performed. Meteor will always transmit an id to identify the object that should be added, updated or removed, so objects that represent Meteor documents must always have an id field. Here we're sticking to the MongoDB convention of naming our id _id.


struct Contact {

    var _id:String?
    var name:String?
    var phone:String?
    var email:String?

    init(id:String, fields:NSDictionary?) {
        self._id = id
        update(fields)
    }

    mutating func update(fields:NSDictionary?) {

        if let name = fields?.valueForKey("name") as? String {
            self.name = name
        }

        if let phone = fields?.valueForKey("phone") as? String {
            self.phone = phone
        }

        if let email = fields?.valueForKey("email") as? String {
            self.email = email
        }
    }
}

Next, we'll create the collection class that will hold our contacts and provide the logic to respond to server-side changes to the documents and the subscription set. SwiftDDP contains an abstract class called AbstractCollection that can be used to build custom collections. Subclassing AbstractCollection allows you to override three methods that are called in response to events on the server: documentWasAdded, documentWasChanged and documentWasRemoved.

class UserCollection: AbstractCollection {

    var contacts = [Contact]()

    // Include any logic that needs to occur when a document is added to the collection on the server
    override public func documentWasAdded(collection:String, id:String, fields:NSDictionary?) {
        let user = User(id, fields)
        users.append(user)
    }

    // Include any logic that needs to occur when a document is changed on the server
    override public func documentWasChanged(collection:String, id:String, fields:NSDictionary?, cleared:[String]?) {
        if let index = contacts.indexOf({ contact in return contact._id == id }) {
            contact = contacts[index]
            contact.update(fields)
            contacts[index] = contact
        }
    }

  // Include any logic that needs to occur when a document is removed on the server
  override public func documentWasRemoved(collection:String, id:String) {
    if let index = contacts.indexOf({ contact in return contact._id == id }) {
        contacts[index] = nil
        }
    }
}

So far, we're able to process documents that have been added, changed or removed on the server. But the UserCollection class still lacks the ability to make changes to both the local datastore and on the server. We'll change that. In the UserCollection class, create a method called insert.

class UserCollection: AbstractCollection {
    /*
    override public func documentWasAdded ...
    override public func documentWasChanged ...
    override public func documentWasRemoved ...
    */

    public func insert(contact: Contact) {

        // (1) save the document to the contacts array
        contacts[contacts._id] = contact

        // (2) now try to insert the document on the server
        client.insert(self.name, document: [contacts.fields()]) { result, error in

            // (3) However, if the server returns an error, reverse the action on the client by
            //     removing the document from the contacts collection
            if error != nil {
                self.contacts[contact._id] = nil
                log.error("\(error!)")
            }

        }

    }
}

The key parts of this method are:

Creating update and remove methods are also easy to create, and follow the same patern as insert. For a more extensive example of the patterns shown here, have a look at MeteorCollection.swift. MeteorCollection is an in-memory collection implementation suitable for simple applications.

Changelog

0.4.0

0.3.2

0.3.1

0.3.0

Version 0.3.0 contains breaking changes

0.2.2.1

0.2.1

0.2.0

Contributing

Pull requests, feature requests are feedback are welcome. If you're using SwiftDDP in a production app, let us know.