Real-Time Communication with Streams Tutorial for iOS

Get down to TCP-level networking and learn about sockets and how to use Core Foundation to build a real-time chat app in this iOS streams tutorial. By Brody Eller.

4.6 (36) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Opening a Connection

Head back over to ChatRoom.swift and, below the property definitions, add the following method:

func setupNetworkCommunication() {
  // 1
  var readStream: Unmanaged<CFReadStream>?
  var writeStream: Unmanaged<CFWriteStream>?

  // 2
  CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,
                                     "localhost" as CFString,
                                     80,
                                     &readStream,
                                     &writeStream)
}

Here’s what’s happening:

The function takes four arguments. The first is the type of allocator you want to use when initializing your streams. You should use kCFAllocatorDefault whenever possible, though there are other options if you run into a situation where you need something that acts a little differently.

Next, you specify the hostname. In this case, you’re connecting to the local machine; if you had a specific IP address for a remote server, you could also use that here.

Then, you specify that you’re connecting via port 80, which is the port the server listens on.

Finally, you pass in the pointers to your read and write streams so the function can initialize them with the connected read and write streams that it creates internally.

  1. First, you set up two uninitialized socket streams without automatic memory management.
  2. Then you bind your read and write socket streams together and connect them to the socket of the host, which is on port 80 in this case.

Now that you’ve got initialized streams, you can store retained references to them by adding the following lines at the end of setupNetworkCommunication():

inputStream = readStream!.takeRetainedValue()
outputStream = writeStream!.takeRetainedValue()

Calling takeRetainedValue() on an unmanaged object allows you to simultaneously grab a retained reference and burn an unbalanced retain so the memory isn’t leaked later. Now you can use the input and output streams when you need them.

Next, you need to add these streams to a run loop so that your app will react to networking events properly. Do so by adding these two lines to the end of setupNetworkCommunication():

inputStream.schedule(in: .current, forMode: .common)
outputStream.schedule(in: .current, forMode: .common)

Finally, you’re ready to open the flood gates! To get the party started, add to the bottom of setupNetworkCommunication():

inputStream.open()
outputStream.open()

And that’s all there is to it. To finish up, head over to ChatRoomViewController.swift and add the following line to viewWillAppear(_:):

chatRoom.setupNetworkCommunication()

You now have an open connection between your client app and the server running on localhost.

You can build and run your app if you want, but you’ll see the same thing you saw before since you haven’t actually tried to do anything with your connection yet.

ios streams tutorial starter chat application

Joining the Chat

Now that you’ve set up your connections to the server, it’s time to actually start communicating! The first thing you’ll want to say is who exactly you think you are. Later, you’ll want to start sending messages to people.

This brings up an important point: Since you have two kinds of messages, you’ll need to find a way to differentiate them.

The Communication Protocol

One advantage of dropping down to the TCP level is that you can define your own “protocol” for deciding whether a message is valid or not.

With HTTP, you need to think about all those pesky verbs like GET, PUT, and PATCH. You need to construct URLs and use the appropriate headers and all kinds of stuff.

Here you just have two kinds of messages. You can send:

iam:Luke

to enter the room and inform the world of your name.

And you can say:

msg:Hey, how goes it, man?

to send a message to everyone else in the room.

This is simple but also blatantly insecure, so don’t use it as-is at work. ;]

Now that you know what the server expects, you can write a method on ChatRoom to allow a user to enter the chat room. The only argument it needs is the desired username.

To implement it, add the following method below the setup method you just wrote inside ChatRoom.swift:

func joinChat(username: String) {
  //1
  let data = "iam:\(username)".data(using: .utf8)!
  
  //2
  self.username = username

  //3
  _ = data.withUnsafeBytes {
    guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
      print("Error joining chat")
      return
    }
    //4
    outputStream.write(pointer, maxLength: data.count)
  }
}
  1. First, you construct your message using the simple chat room protocol.
  2. Then, you save the name so you can use it to send chat messages later.
  3. withUnsafeBytes(_:) provides a convenient way to work with an unsafe pointer version of some data within the safe confines of a closure.
  4. Finally, you write your message to the output stream. This may look a little more complex than you assumed, but write(_:maxLength:) takes a reference to an unsafe pointer to bytes as its first argument, which you created in the previous step.

Now that your method is ready, open ChatRoomViewController.swift and add a call to join the chat at the bottom of viewWillAppear(_:).

chatRoom.joinChat(username: username)

Now, build and run your app. Enter your name, and then tap return to see…

ios streams tutorial starter chat application

The same thing?!

Now, hold on, there’s a good explanation. Go to your terminal. Under Listening on 127.0.0.1:80, you should see Brody has joined, or something similar if your name happens not to be Brody.

This is good news, but you’d rather see some indication of success on the phone’s screen.

Reacting to Incoming Messages

The server sends incoming messages like the join message you just sent to everyone in the room, including you. As fortune would have it, your app is already set up to show any type of incoming message as a cell in the ChatRoomViewController‘s table of messages.

All you need to do is use inputStream to catch these messages, turn them into Message objects, and pass them off to let the table do its thing.

In order to react to incoming messages, the first thing you’ll need to do is have your chat room become the input stream’s delegate.

To do this, go to the bottom of ChatRoom.swift and add the following extension.

extension ChatRoom: StreamDelegate {
}

Now that you’ve said you conform to StreamDelegate, you can claim to be inputStream‘s delegate.

Add the following line to setupNetworkCommunication() directly before the calls to schedule(in:forMode:):

inputStream.delegate = self

Next, add this implementation of stream(_:handle:) to the extension:

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch eventCode {
    case .hasBytesAvailable:
      print("new message received")
    case .endEncountered:
      print("new message received")
    case .errorOccurred:
      print("error occurred")
    case .hasSpaceAvailable:
      print("has space available")
    default:
      print("some other event...")
    }
}