Hot&Cold UX

I happened to watch the App Prototyping WWDC '14 session and was pleasantly surprised that their methods are similar to mine.

Essentiallay it's:

  • As cheaply as possible, make some mock ups.
  • Show people
  • Get feedback

Hot&Cold is a bit different, since I had two technologies I wanted to try out first.

I managed to find some time between unpacking boxes and full time dadding to draw out some screens and run them by my crack focus group (Wife, 1 yr old, cat). I ended up covering 4 legal sheets with screens, ended up refinind them to these:

4 screens total. Toggle feature to switch between hide and seek, taunt functionality to harass your friend, some room for some neat transitions between screens.


Swift Multipeer Rewrite

The amount of code that ran the Multipeer Protype was small, so I decided to just rewrite the damn thing in Swift to get a feel for the new language.

I decided for the hell of it to write something up journal style. Very stream of though-y, not sure how easy it is to read or not.

I posted the journal here

I posted the code on GitHub

I really like Swift, it's amazing just how much extranious code just falls away when you start getting the hang out it. It really reminds me much more of writing in Javascript or Ruby.

I've got to hand it to Apple, they didn't make any of my Cocoa knowledge obsolete at all. It's writing the same calls against the same frameworks tha that's amazing.


Multipeer Networking and You

The goal of this prototype is to get two devices communicating via multipeer networking. If Apple's done their job right (Framework development + documentation) it should be easy, right?

Once you wrap your head around how the whole system is implemented then yes, it actually is fairly easy to get multiple devices communicating.

First off, let's lay our our user story/use case:

Two devices are connected via multipeer, each one has a button (which is activated when a connection is secured) which will increment the counter on the other device.  

Apple's Multipeer Framework page is a bit light but it does lay out the general pieces that we need.

  • Session objects handle the communications.
  • Advertiser objects tells others they're available.
  • Browser objects browse for advertised devices.
  • Peer ID's allow for unique identification.

One of the interesting things about the multipeer networks is that unlike a traditional client/server network each node could be advertising and discovering at the same time.

This lead me to one concept that I had to wrap my head around before I could really grok this concept: It's the browsing device that invites the advertiser to a session.

Initially, I was thinking of the advertiser as more of a server and the browser as a client. Wrong, wrong, wrong. Thinking like that made it even more difficult for me to really understand what was going on under the hood.

Let's describe the workflow:
- You declare a service identifier (just an NSString constant) so your app knows what to advertise/browse. - The app starts both advertising and browsing for that service at the same time. - If the app finds a peer, create a session and invite that peer to it. - If the app receives an invitation to a session, accept it. - Stop advertising or discovering if either of these things happens, restart is the peer is lost. - Start sharing that sweet, sweet data.

As a warning, it's what my app's implementing is an exceptionally simplified version of what this framework can do. It's assuming that there are only two peers that always want to connect to each other.

  • The app should be alerting users when you receive a peering invitation, allow them to decline.
  • You probably keep a list of found peers stored so that you can act when the peer is lost.

In true Apple Framework fashion, there's the 'Simple to implement but boring looking' Apple standard and the 'Handle everything yourself with base classes' approach. A lot of the examples out there is using the Apple way, which is to use MCAdvertiserAssistant to handle the advertising and MCBrowserViewController to browse for available peers.

The issue with each of these is that I was hoping to implement more of a SpaceTeam-esque method of matchmaking so I realized that I would have to roll my own.

Code - ASWASWLobbyInitViewController.m

Everything is contained inside one view controller.

Imports + Interface + Implementation stuff

@import MultipeerConnectivity;

static NSString * const HotColdServiceType = @"hotcold-service";

@interface ASWLobbyInitViewController () <MCNearbyServiceAdvertiserDelegate, MCSessionDelegate, MCNearbyServiceBrowserDelegate>

@property IBOutlet UILabel *myDeviceName;
@property IBOutlet UILabel *theirDeviceName;
@property IBOutlet UILabel *theirButtonCounter;
@property IBOutlet UIButton *incrementCounterButton;

@property MCSession *session;
@property MCNearbyServiceAdvertiser *advertiser;
@property MCNearbyServiceBrowser *browser;
@property MCPeerID *localPeerID;
@property NSMutableArray *connectedPeers;

@end


@implementation ASWLobbyInitViewController {
    NSNumber *buttonCounter;
}

HotColdServiceType is simply a string we'll be using later as the service identifier.

We declare ourselves to be able to handle MCNearbyServiceAdvertiserDelegate, MCSessionDelegate and MCNearbyServiceBrowserDelegatedelegate messages.

We have our outlets for our our labels and a few private properties to keep instances of our session, advertiser, browser, peerID and an array of connected peers.

One private variable is an NSNumber of the button counter. It's the value we'll be sending across.

viewDidLoad

- (void)viewDidLoad {
    [super viewDidLoad];

    self.localPeerID = [[MCPeerID alloc] initWithDisplayName:[[UIDevice currentDevice] name]];

    self.myDeviceName.text = @"";
    self.theirDeviceName.text = @"";
    self.theirButtonCounter.text = @"0 times";
    self.incrementCounterButton.enabled = NO;
    buttonCounter = [[NSNumber alloc] initWithInt:0];
    self.connectedPeers = [[NSMutableArray alloc] init];

    // Browser for others
    self.browser = [[MCNearbyServiceBrowser alloc] initWithPeer:self.localPeerID
                                                    serviceType:HotColdServiceType];
    self.browser.delegate = self;

    // Advertise to others
    self.advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:self.localPeerID
                                                        discoveryInfo:nil
                                                          serviceType:HotColdServiceType];
    self.advertiser.delegate = self;

}

Right now, I'm using the device name as the device's Peer ID. In the future I think I'll be generating something a bit more fun.

After that, we set some default values and initialize the browser and the advertiser.

We're ready to get this show on the road.

viewWillAppear

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    self.myDeviceName.text = self.localPeerID.displayName;

    [self.browser startBrowsingForPeers];
    [self.advertiser startAdvertisingPeer];
}

We set our display name on the label and start both browsing and advertising. From here things get event based.

MCNearbyServiceAdvertiserDelegates

-(void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID
      withContext:(NSData *)context
invitationHandler:(void (^)(BOOL, MCSession *))invitationHandler {  
    // Creates a session anytime someone connects using the service.
    NSLog(@"Received Invitation from %@", peerID.displayName);

    if (!self.session) {
        self.session = [[MCSession alloc] initWithPeer:self.localPeerID
                                      securityIdentity:nil
                                  encryptionPreference:MCEncryptionNone];
        self.session.delegate = self;
        invitationHandler(YES, self.session);

        [self.advertiser stopAdvertisingPeer];
        [self.browser stopBrowsingForPeers];
    }

}

When a browsing peer sends you an invitation, this method will fire.

Locking it down with an if (!self.session) means that we won't respond if we've already connected to a session. Since we're purposefully locking this down to 2 peers, we're good.

When we initialize session property we pass it our local peer id this caused me a great deal of confusion. I assumed that you create your session with the peerID value being passed in by your peer.

The docs are actually pretty clear about this:

Create an MCPeerID object that represents the local peer, and use it to initialize the session object.  

After setting the delegate for the session to self, we then have to call the handler block with a true boolean and the session object as parameters and then stop both advertising and browsing.

MCNearbyServiceBrowserDelegates

-(void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info {
    NSLog(@"FOUND PEER %@", peerID.displayName);

    if (!self.session){
        self.session = [[MCSession alloc] initWithPeer:self.localPeerID
                                      securityIdentity:nil
                                  encryptionPreference:MCEncryptionNone];
        self.session.delegate = self;

        [browser invitePeer:peerID toSession:self.session withContext:nil timeout:5];

        [self.advertiser stopAdvertisingPeer];
        [self.browser stopBrowsingForPeers];
    }
}

-(void)browser:(MCNearbyServiceBrowser *)browser lostPeer:(MCPeerID *)peerID {
    NSLog(@"LOST PEER %@", peerID);

    // Kill the session
    self.session = nil;

    // Start looking again.
    [self.advertiser startAdvertisingPeer];
    [self.browser startBrowsingForPeers];
}

On the browsing side, we're simply looking for any advertised services, inviting them to a session (Notice that on this end we're also initializing it with our local session ID) setting our delegate, and stopping both advertising and browsing once one is found.

Now, if we lose the peer after finding it, we're simply restarting the advertising and browsing services.

#pragma mark - MCSessionDelegate

-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID {
    NSString *message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"Message received: %@", message);
    dispatch_async(dispatch_get_main_queue(),^ {
        self.theirButtonCounter.text = message;
    });
}

-(void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream
      withName:(NSString *)streamName
      fromPeer:(MCPeerID *)peerID {

}

-(void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName
      fromPeer:(MCPeerID *)peerID
  withProgress:(NSProgress *)progress {
}

-(void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName
      fromPeer:(MCPeerID *)peerID
         atURL:(NSURL *)localURL
     withError:(NSError *)error {
}

-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
    NSArray *stateStringRepresentation = @[@"MCSessionStateNotConnected", @"MCSessionStateConnecting", @"MCSessionStateConnected" ];

    NSLog(@"SESSION STATE CHANGE: %@", stateStringRepresentation[state] );

    if (state == MCSessionStateConnected) {
        NSLog(@"Connected to %@", peerID.displayName);

        [self.connectedPeers addObject:peerID];

        dispatch_async(dispatch_get_main_queue(),^ {
            self.theirDeviceName.text = peerID.displayName;
            self.incrementCounterButton.enabled = YES;
        });

        [self.view setNeedsDisplay];
    }
}

The two important delegate events are didReceiveData and didChangeState.

didReceiveData is simply called when some data is returned from your peer, It's sent as NSData, so you have to change it back to a NSString.

You also see that I'm setting the text on the label in the main thread using the main queue. If you want the UI to be updated immediately you need to make sure that you call those on the main thread.

Once a session has been created, you need to watch for MCSessionStateConnected to be returned in the didChangeState method. This means that you're good to go to begin sending information across.

I'm enabling the button and setting the peer label on the main queue once we're connected.

'Press This' Button Action

- (IBAction)incrementCounterAndSend:(UIButton *)sender {
    buttonCounter = @([buttonCounter intValue] + 1);
    NSString *message = [NSString stringWithFormat:@"%d times", [buttonCounter integerValue]];
    NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding];
    NSError *error = nil;
    if (![self.session sendData:data
                        toPeers:self.connectedPeers
                       withMode:MCSessionSendDataReliable
                          error:&error]) {
        NSLog(@"[Error] %@", error);
    }
}

Pretty simple stuff. Just incrementing the button integer by one, creating a 'X times' string and sending that across the wire.

That's it! You end up with a 2 peer system as soon as you arrive on the view controller. I haven't tested it with more than 2 though.

Now I think I'll rewrite this in Swift.


Hot&Cold - Prototype Code Review

Aaand I have iBeacons. (v0.1)

All told, I had the iBeacon code up and running in a little less that 2 hours. Unfortunately, those 2 hours were spread over three weeks as my 9 month old son decided to go through a sleep regression. Babies!

Back to the code.

The code itself was simple after reading the API docs. I've broken out the applications into three beautifully designed screens.

The app is using Storyboards.

ASWChoiceViewController

Honestly, nothing too much of interest here. The app has a couple of segues set up. Each of the two buttons are hooked up to the same IBAction call and depending on which one you select you end up going to the required screen.

- (IBAction)gotoSegue:(UIButton *)sender {
    NSString *segueIdent = @"";
    if ([sender.titleLabel.text isEqualToString:@"Hide"]) {
        segueIdent = @"toSend";
    } else {
        segueIdent = @"toReceive";
    }
    [self performSegueWithIdentifier:segueIdent sender:self];
}

ASWDefaults

The implementation of this is almost identical to Apple's AirLocate APLDefaults file

ASWDefaults.m

//ASWDefaults.h
extern NSString *BeaconIdentifier;  
//ASWDefaults.m
NSString *BeaconIdentifier = @"com.example.ampersand-softworks.HotCold";  

The BeaconIdentifier is simply being stored as a global string constant and is declared in the header file.

-(id) init { 
    self = [super init];
    if (self){
        _supportedProximityUUIDs = @[[[NSUUID alloc] initWithUUIDString:@"D9EED498-BFDB-43C0-8B55-D06BB74C430B"]];
        _defaultPower = @-59;
    }
    return self;
}

The init method populates an array of supported UUIDs, I simply used the uuidgen command in the terminal to get myself a new one and added it to the list. This is future-proofing the app as well since adding support for a whole new set of beacons is trivial.

+(ASWDefaults*) sharedDefaults {    
    static id sharedDefaults = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedDefaults = [[self alloc] init];
    });
    return sharedDefaults;
}

In the past I've used the @synchronized in order to thread this. This is my first chance to use GCD and blocks in this manner. I heart blocks.

- (NSUUID *) defaultProximityUUID {
    return _supportedProximityUUIDs[0];
}

Simply returns the one (and only) uuid that we're using. Again, future proofing.

ASWDefaults.h

Simply exposes the stuff that was set up.

extern NSString *BeaconIdentifier;

@interface ASWDefaults : NSObject

+(ASWDefaults *) sharedDefaults;

@property (nonatomic, copy, readonly) NSArray *supportedProximityUUIDs;
@property (nonatomic, copy, readonly) NSUUID *defaultProximityUUID;
@property (nonatomic, copy, readonly) NSNumber *defaultPower;

@end

ASWHideViewController

Let's get our device broadcasting as an iBeacon. I was surprised at just how little code was needed in order to get the beacon broadcasting with the required region information.

@import CoreLocation;
@import CoreBluetooth;

Awwwwww yeah, precompiled header modules. I'm pretty sure the guy giving the talk on modules at WWDC mentioned that all #import calls are mapped to @import behind the scene.

CBPeripheralManager *perhipheralManager = nil;  
CLBeaconRegion *region = nil;

NSDictionary *beaconPerhipheralData;  
NSNumber *power = nil;  

We need an instance of the CBPerhipheralManager and the CLBeaconRegion classes to begin broadcasting.

  • CBPeripheralManager is what actually handles the bluetooth broadcasts.
  • CLBeaconRegion gets fed the uuid, major version, minor version and identifier so the peripheral manager knows what values to throw out into the aether.

The beaconPerhipheralData dictionary and the power variables are simply there to hold setup values.

@interface ASWHideViewController () <CBPeripheralManagerDelegate>

@property NSUUID *uuid;
@property NSNumber *major;
@property NSNumber *minor;

@end
  • We set ourselves up to receive any CBPeripheralManager events that get fired
  • Set up some private variables.
- (void)viewDidLoad {
    [super viewDidLoad];

    self.uuid = [ASWDefaults sharedDefaults].defaultProximityUUID;
    self.major = [NSNumber numberWithShort:0];
    self.minor = [NSNumber numberWithShort:0];
    power = [ASWDefaults sharedDefaults].defaultPower;

    region = [[CLBeaconRegion alloc] initWithProximityUUID:self.uuid
                                                     major:[self.major shortValue]
                                                     minor:[self.minor shortValue]
                                                identifier:BeaconIdentifier];
    beaconPerhipheralData = [region peripheralDataWithMeasuredPower:power];
}

Inside the viewDidLoad we pull the uuid, major, minor, identifier and power values from the ASWDefaults file and alloc a new instance of the CLBeaconRegion. From there, we store that value in a dictionary for future peripheral manager use.

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    if (!perhipheralManager) {
        perhipheralManager = [[CBPeripheralManager alloc] initWithDelegate:self
                                                                     queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
                                                                   options:nil];
    } else {
        perhipheralManager.delegate = self;
    }
}

Lazy load the peripheral manager with a default background thread.

-(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
    if (peripheral.state == CBPeripheralManagerStatePoweredOn) {
        NSLog(@"TRANSMITTING");
        [perhipheralManager startAdvertising:beaconPerhipheralData];
    } else if (peripheral.state == CBPeripheralManagerStatePoweredOff){
        NSLog(@"Transmission Ceased");
        [perhipheralManager stopAdvertising];
    }
}

Here's where the magic happens. If bluetooth is turned on we begin broadcasting the iBeacon region information using startAdvertising. Just like that, we have an iBeacon.

ASWSeekViewController

The first bit of surprising information that I discovered is that all of the iBeacon receiving code is handed by a Core Location locationManager instance. The minute that you initialize it, the user is asked if they will allow you app to know your location.

It makes sense, you could get someone's location within a centimetre with a combination of an iBeacon and GPS. It was just a bit unexpected that the first time I ran the app.

Right now, the prototype simply shows the distance to the beacon (the value is actually the accuracy value from a CLBeacon).

@interface ASWSeekViewController () <CLLocationManagerDelegate>

@property (weak, nonatomic) IBOutlet UILabel *howClose;

@property CLLocationManager *locationManager;
@property NSMutableDictionary *rangedRegions;

@end

The CLLocationManager does all of the magic and this view controller is it's delegate. Some other properties are simply set up.

- (void)viewDidLoad {
    [super viewDidLoad];

    self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;

    self.rangedRegions = [[NSMutableDictionary alloc] init];
    for (NSUUID *uuid in [ASWDefaults sharedDefaults].supportedProximityUUIDs) {
        CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid identifier:[uuid UUIDString]];
        self.rangedRegions[region] = [NSArray array];
    }
}

The loop here is pretty interesting.

What we're doing is grabbing all of the beacon UUIDs from ASWdefaults file and filling the rangedBeacons array with CLBeaconRegion instances containing those values. This is used in the locationManager:didRangeBeacons and locationManager:startRangingBeaconsInRegion methods.

-(void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    for (CLBeaconRegion *region in self.rangedRegions) {
        [self.locationManager startRangingBeaconsInRegion:region];
    }
}

This is where the magic happens. Just loop through the CLBeaconRegions in the rangedBeacons dictionary and start listening for events.

-(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {

    if ([beacons count] > 0) {
        // Let's assume we're getting one beacon for now.
        CLBeacon *beacon = beacons[0];
        self.howClose.text = [NSString stringWithFormat:@"%f", beacon.accuracy];
    } else {
      self.howClose.text = @"No Beacons Found";
    }

}

If a beacon is found we simply show the accuracy value on screen. I do get weird instances when I get -1.000 as the value. It seems to happen if there's a lot of interference.

I'll have to do more testing with the accuracy values to see what can happen.

Final thoughts

  • The app will probably crash right now if you have bluetooth disabled. I'll have to lock down the app at various points to alert the user that it's required. Not too bad.
  • I'll have to figure out the messaging for how the app asks for access to your location. It might be a bit surprising if the user gets the location request since location = GPS (at leas in my mind) and I'm not using that at all.
  • The location manager in ASWSeekViewController assumes that it'll find only one beacon. The rest of the code is future proof but this one is a hack. Need to finish it off.
  • The accuracy value I'm getting back on the beacons returned from locationManager:didRangeBeacons:inRegion method isn't updated too regularly. This is going to be perfectly cromulent for the app's use case since I won't be displaying the distance values directly. But I will be able to use these values to figure out when HOT or COLD will be yelled at the person.
  • Having [locationManager startRangingBeacons] in the viewDidAppear is for the prototype only. Need to better figure out a way handling the activation of the search.
  • Each CLBeacon has a near/far/immediate proximity value available for use. Even though Apple bragged about the iBeacon's "centimetre accuracy" they seem to be recommending you use these values instead.

Words by brett ohland

Hot/Cold - Oh Look: A Repo.

I decided this morning that there's really nothing wrong in posting the source of the app up on GitHub for the time being. Really, it's in the middle of heavy development and I've never really developed anything in the open before. What's the point of keeping a dev blog if I can't reference chunks of code?

The repo has been created

Another thing that might be interesting is that I think I'll be linking directly to specific commits in order to give a bit of historical context to certain posts. Not sure if I'll keep doing this but it seems like an interesting idea.

It's that awkward first set of check-ins

Mattt over at NSHipster had a great post on Multi-peer connectivity that really make me interested in making this application communicate both ways. The 'seeker' would only get "HOT" or "COLD" verbal commands but the Hider could get a distance measurement.

Taking it the next step you could even have multiple Seekers looking for one Hider and all of their distance measurements could be displayed (Or just show the motion tracker from the movie Alien to really freak them out).


Words by brett ohland

App #1: 'Hot/Cold' App Outline

The first and simplest app idea that's been rolling around my brain uses iBeacons. I attended a couple of sessions at WWDC 2014 about them and have been really interested in playing around with them ever since.

So here's the app idea (it's kind of a joke): You have two iOS devices. One is the 'hide' and one is the 'seek'. The 'seek' device does nothing but yell HOT! HOT! HOT! COLD! COLD! as you get closer or further… I didn't say it was a good idea, just that it was an idea.

(IAP idea: Novelty voices?)

Getting the 'seek' app up and running should be pretty trivial, Apple has their Air Locate app that shows just how simple it is to listen for iBeacon signals. The neat thing is that with one UUID there could be multiple people/apps looking for one 'hide' app instance.

On the 'seek' side of things it gets a bit more interesting. The idea for the app would be for two kids to be using them to have a kind of nerdy hide and go seek game. I was thinking that the hide app could say some funny things or give updated information on how far the seek app(s) are away from you. Since iBeacons are supposed to be a one way communication path (broadcast only) that would mean that I would need to look into being both a iBeacon broadcaster as well as an iBeacon receiver. Is that possible? I guess we'll see.

Step one's going to be just getting the application scaffolding up and running and starting with a basic 'one is hide and one is seek' prototype.


Words by brett ohland