Swift/iOS client guide

The official Swift client handles all communication in realtime with the server and is specifically optimized for iOS projects. It implements all features in the server and is compatible with Swift 3.1+. To work with the Swift client you'll need the Swift distribution and an editor/IDE like Atom or XCode 8.3+.

Download

Releases for the client are managed on GitHub. You can use the Swift package manager to add the code as a dependency for your project.

For upgrades you can see changes and enhancements in the CHANGELOG before you update to newer versions.

Help and contribute

The Swift client is open source on GitHub. Please report issues and contribute code to help us improve it.

Install

There are a few ways to import the Swift client into your project:

  1. Cocoapods

Add the client as a dependency to your "Podfile":

use_frameworks!
pod 'Nakama', '~> 0.2'

Ensure that the dependencies are built as Frameworks. Download and integrate it into your Xcode project:

pod install
  1. Swift Package Manager

Add the client as a dependency to your "Package.swift" file.

let package = Package(
  // ...
  dependencies: [
    .Package(url: "https://github.com/heroiclabs/nakama-swift.git", Version(0,2,0)),
  ]
)

Setup

The client object is used to execute all logic against the server.

import Nakama

public class NakamaSessionManager {
  private let client: Client;

  init() {
    client = Builder("defaultkey")
        .host("127.0.0.1")
        .port(7350)
        .ssl(false)
        .build()
  }
}

You can also use the shorthand builder for the client.

Note

By default the client uses connection settings "127.0.0.1" and 7350 to connect to a local Nakama server.

let client : Client = Builder.defaults(serverKey: "defaultkey")

Authenticate

With a client object you can authenticate against the server. You can register or login a user with one of the authenticate options.

To authenticate you should follow our recommended pattern in your client code:

   1. Build an instance of the client.

let client : Client = Builder.defaults(serverKey: "defaultkey")

   2. Login or register a user.

Tip

It's good practice to cache a device identifier on iOS when it's used to authenticate because they can change with device OS updates.

let message = AuthenticateMessage(device: deviceId!)
client.login(with: message).then { session in
  // connect to the server with the session
}.catch{ err in
  if (err is NakamaError) {
    switch err as! NakamaError {
    case .userNotFound(_):
      self.client.register(with: message).then { session in
        // connect to the server with the session
      }.catch{ err in
        print("Could not register: %@", err)
      }
    default:
      break
    }
  }
  print("Could not login: %@", err)
}

In the code above we use AuthenticateMessage.device(id: deviceID) but for other authentication options have a look at the code examples.

The client uses promise chains for an easy way to execute asynchronous callbacks.

   3. Store session for quick reconnects.

We can replace the callback marked in step 2 with a callback which stores the session object on iOS.

let defaults = UserDefaults.standard
defaults.set(session.token, forKey: "session")

A full example class with all code above is here.

Send messages

When a user has been authenticated a session is used to connect with the server. You can then send messages for all the different features in the server.

This could be to add friends, join groups and chat, or submit scores in leaderboards, and matchmake into a multiplayer match. You can also execute remote code on the server via RPC.

The server also provides a storage engine to keep preferences and other records owned by users. We'll use storage to introduce how messages are sent.

let saveGame = "{\"progress\": 50}".data(using: .utf8)!
let myStats = "{\"skill\": 24}".data(using: .utf8)!

let bucket = "myapp"
var message = StorageWriteMessage()
message.write(bucket: bucket, collection: "saves", key: "savegame", value: saveGame)
message.write(bucket: bucket, collection: "saves", key: "mystats", value: myStats)
client.send(message: message).then { list in
  for recordId in list {
    NSLog("Stored record has version '%@'", recordId.version)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}

Have a look at other sections of documentation for more code examples.

Promise chains

The client uses promises to represent asynchronous actions with the PromiseKit library. This makes it easy to chain together actions which must occur in sequence and handle errors.

A deferred object is created when messages are sent and can attach callbacks and error handlers for working on the deferred result.

var message = StorageWriteMessage()
message.write(bucket: bucket, collection: "saves", key: "savegame", value: saveGame)

let promise : Promise<StorageRecordID> = client.send(with: message)

You can chain callbacks because each callback returns a Promise<T>. Each method on the Client returns a Promise<T> so you can chain calls with .then { }.

promise.then { _ in
  return client.send(with: message)
}.then { _ in
  //...
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}

Logs and errors

The server and the client can generate logs which are helpful to debug code. To log all messages sent by the client you can enable "trace" when you build a "Client".

let client : Client = new Builder("defaultkey")
    .trace(true)
    .build();

Every error in the Swift client implements the "NakamaError" class. It contains details on the source and content of an error:

let promise = ...

promise.catch { err in
  if (err is NakamaError) {
    switch err as! NakamaError {
    case .storageRejected(let msg):
      print("Storage rejected: %@", msg)
    case ...
    default:
      break
    }
  }
  print("Operation failed: %@", err)
}

Full example

internal class NakamaSessionManager {
  public let client : Client
  private var session : Session?

  private static let defaults = UserDefaults.standard
  private static let deviceKey = "device_id"
  private static let sessionKey = "session"

  internal init() {
    client = Builder.defaults(serverKey: "defaultkey")
  }

  func start() {
    restoreSessionAndConnect()
    if session == nil {
      loginOrRegister()
    }
  }

  private func restoreSessionAndConnect() {
    // Lets check if we can restore a cached session
    let sessionString : String? = NakamaSessionManager.defaults.string(forKey: NakamaSessionManager.sessionKey)
    if sessionString == nil {
      return
    }

    let session = DefaultSession.restore(token: sessionString!)
    if session.isExpired(currentTimeSince1970: Date().timeIntervalSince1970) {
      return
    }

    connect(with: session)
  }

  private func loginOrRegister() {
    var deviceId : String? = NakamaSessionManager.defaults.string(forKey: NakamaSessionManager.deviceKey)
    if deviceId == nil {
      deviceId = UIDevice.current.identifierForVendor!.uuidString
      NakamaSessionManager.defaults.set(deviceId!, forKey: NakamaSessionManager.deviceKey)
    }

    let message = AuthenticateMessage(device: deviceId!)
    client.login(with: message).then { session in
      NakamaSessionManager.defaults.set(session.token, forKey: NakamaSessionManager.sessionKey)
      self.connect(with: session)
      }.catch{ err in
        if (err is NakamaError) {
          switch err as! NakamaError {
          case .userNotFound(_):
            self.client.register(with: message).then { session in
              self.connect(with: session)
              }.catch{ err in
                print("Could not register: %@", err)
            }
            return
          default:
            break
          }
        }
        print("Could not login: %@", err)
    }
  }


  private func connect(with session: Session) {
    client.connect(to: session).then { _ in
      self.session = session

      // Store session for quick reconnects.
      NakamaSessionManager.defaults.set(session.token, forKey: NakamaSessionManager.sessionKey)
      }.catch{ err in
        print("Failed to connect to server: %@", err)
    }
  }
}