Playing with iOS MapKit Part 4: Race Conditions, Other Bugs, Wrap-up

Playing With MapKit

Part 1 MapKit Tutorials 

Part 2 Reverse Geocoding and Custom Annotation Callouts

Part 3 Making it Pretty

Part 4 Race Conditions, Other Bugs, and Wrap-up

As the functionality and the look was coming into place, I took note of bugs that I saw.

The most insidious bugs are ones that don’t happen all the time or under conditions (ex. when internet is especially slow or the computer is especially fast). One example is the race condition.

What is a Race Condition?

A race condition is a situation when the sequence of asynchronous responses results in an undesired effect.

My Example

Very quickly after starting the app, I was tapping the current location dot and I was seeing “Current Location” in the bubble. It should have said the address that the address was closest to. If I waited a couple seconds to tap the current location, it would display the address as expected.

This led me to believe that I had a race condition.

Under normal conditions:

  1. The map updates the user location calling its delegate mapView:didUpdateUserLocation: which starts the reverseGeocoding call based on the user’s location.
  2. The reverseGeocoding response is received and the completion block sets the annotation’s title with the address of the user location.
  3. When the user location dot is tapped, it will display a custom bubble using the annotation’s title in the label.

Usually the internet is fast enough that the reverseGeocoding response returns before the user location dot is tapped.

The two things that are racing are

  1. the reverseGeocoding network call and
  2. the tap of the current location dot.

If 1 finished first, then the app would work as intended. If 2 finished first, the experience would be suboptimal.

Design Choice

One way to fix this is to update the label when the reverseGeocoding network call came back.

At the end of the completion block, I added a method call to updatePinCallout, which pretends that the user location dot was tapped again and reloads the address.

- (void)updatePinCallout
{    
  if (self.userLocationPin.selected) {
   [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    [self.userLocationPin setSelected:YES animated:YES];
   }];
  }
}

Just remember to use the mainQueue to retap the dot or else the address won’t update on screen.

This change also fixed another bug that happened when the user location would jump when WIFI was enabled and the address label would not update to the new address.

Alternative Choice

Another thing that I found solved the problem pretty well was to zoom the map to the user location immediately, so that the animation didn’t even give a chance to tap the user location dot.

Other Bugs

Writing code for multiple scenarios unnecessarily

When I first implemented zooming to fit destinations, I only zoomed when the location was outside of the visible region. I realized that sometimes this would not be sufficient because sometimes the map would be zoomed way out, so I decided to account for that scenario too. I would zoom in if the destination was in the original visible area. This got complicated quickly.

Eventually, I realized that it was too much and just made it always zoom to the destination. The code went from 14 lines to 1.

Simpler is Better

Testing Days Later

I thought I was done with the project. Wrong. A few days later, I was trying out the app on the bus and came across two bugs.

  1. I was on a bus and the location was updating very quickly and constantly centering the map on the new location. The problem was that I wanted to look around the map and couldn’t do that without it changing while I was using it.
    • This is easily fixed by only zooming on the time the map loads.
  2. I was looking for a Pret A Manger. When I saw the results page, I found it utterly useless because all of them said the same thing and I couldn’t tell which location was where.
    • I think it would have been cool to add some directions as to how far each location was from my current location and in which direction or add the cross streets.

As the Pragmatic Programmer said:

“Test your software, or your users will.”

Where to next?

There’s certainly a lot of ways this can go. Add friends, add Facebook integration, add Yelp results, use Google Maps. That would be fun to do, but would lack direction.

For a product to be real, it must have a real use.

The way that this app fits into my life has something to do with lunch (ergo the name LunchTime). In an effort to build good habits, one of them is to walk 10K steps a day. As I’ve discovered, this means walking 1.3 hours a day. If I don’t plan to walk, I won’t get near 10K.

So over lunch, I like to walk 10 to 15 minutes away and back. I think it would be cool to draw a diamond around your current location to see how far you can walk in 5 and 15 minutes and show how far each location is.

Options abound. Looking forward to the next expedition.

It’s been fun working with MapKit. Time to play around with something else. Who knows? I might be looking at WatchKit, EventKit, or something else entirely.

Playing with iOS MapKit Part 3: Making it Pretty

Playing With MapKit

Part 1 MapKit Tutorials 

Part 2 Reverse Geocoding and Custom Annotation Callouts

Part 3 Making it Pretty

Part 4 Race Conditions, Other Bugs, and Wrap-up

After getting down the basic functionality, I wanted to polish up the app to give it a quality feel and to dig deeper into the nitty-gritty details of MapKit.

So I made a list of things that could be improved and proceeded to knock each of them out one by one.

To do list

*I had the most fun with this one.

  1. Remove the jumpy effect on loading
  2. Move bar buttons from toolbar to buttons on the map
  3. Make the callout for the first search result come up
  4. Make the zip code and city on the same line
  5. Make the map center and zoom when the current location is tapped
  6. After clicking the textbox, resign the first responder when the map is clicked
  7. Add Directions
  8. *Zoom to include the destination in the Routeviewcontroller
  9. Intelligently choose walking vs driving directions
  10. Make the starting region of the route, the same as that of the previous view

1. Remove the jumpy effect on loading

When the app loaded, the default map showed the United States and when the user location was updated, it would move center on the current location, leading to a jumpy effect (I didn’t know about setCenterCoordinate: animated: at the time). The jump came about a couple seconds after opening the app depending on the internet connection and lead to a laggy feeling.

Design Choice

Initially, I chose to not center the map on the current location because it was a quick fix. I let the user tap the current location to zoom in. I chose this because it was the easiest fix.

After some iterations, I decided it was better just to zoom in automatically.

Back to list

2. Move bar buttons from toolbar to buttons on the map

Many of the map apps that I’ve seen have floating buttons on the map instead of a toolbar. I planned to do this, but it didn’t seem worth the effort.

Design Choice

After looking at the default maps app and seeing that it used a toolbar too, I decided to save time and stick with the toolbar. I just separated the buttons to make it more symmetrical.

Back to list

3. Make the callout for the first search result come up

Before, I had the callout for the last search result come up. This lead to the map whipping to the last location when it was off the visible region.

Design Choice

Make the first search result come up.

I used enumerateObjectsUsingBlock to save the first item. In doing this, I found out that I needed to set my firstAnnotation object to __block, so that the change would be visible outside of the block. (More on StackOverflow.)

How do I select a MKPointAnnotation?

[self.mapView selectAnnotation:firstAnnotation 
animated:YES];

Back to list

4. Make the zip code and city on the same line

Custom Callout View

The view for the address callout was too small to show the whole line for city, state, and zip code.

Design Choice

The easiest way to fix this was to make the view wider and shorten the zip to five numbers.

Afterwards, I ran into problems with special places that had an extra line for the placename, so I had to add extra code to parse the formatted address lines in cases where there were two or three lines.

In the final version, the callout view is now a bit too big, so future work may be nice to set the size dynamically and even animated.

Back to list

5. Make the map center and zoom when the current location is tapped

Easy, call the zoomIn method that the zoom in button calls.

Back to list

6. After clicking the textbox, resign the first responder when the map is clicked

The touchesBegan: withEvent: method detects when I touch outside the textfield onto the map, so this was easy to fix.

- (void) touchesBegan:(NSSet *)touches withEvent:
(UIEvent *)event
{
    [self.searchText resignFirstResponder];
}

Back to list

7. Add Directions

With the route shown, it was natural to want to show the directions and the distances involved.

Design Choice

The easiest thing was to show the directions in a tableViewController and segue to it from a UIBarButton on the navigation bar of the directions page. This is pretty standard, so I just passed the instructions to a table view controller and displayed it in the cells.

The distances are currently in meters. That’s not very intuitive so it would be nice to have this in blocks.

Back to list

*8. Zoom to include the destination in the RouteViewController

The idea is that when I selected a venue which was outside the visible range, I wanted the map to zoom to fit the destination and my current location. While tedious, executing this was not hard when I saw down to work backwards from

[self.routeMap setRegion:self.adjustedRegion 
animated:YES];

How do I zoom in to fit two locations on a map?

Basically,

  • Get the coordinates for your location and the destination location.
  • Find the difference in latitude and longitude and scale this up by a multiplier. A multiplier of 1 will have both locations on the edges of the screen.
  • Make sure to get the absolute difference. Negative numbers are no good.
  • Make a MKCoordinateSpan with the ABSOLUTE differences in latitude and longitude.
  • Find the average latitude and longitude to get the center for the region.
  • Make the MKCoordinateRegion with the centerCoordinate and the span.
  • Find the adjustedRegion by calling regionThatFits on the region in the last step.
  • Set the MKMapView to that adjustedRegion.
- (void)resizeRegionWithDestination:(MKMapItem *)destination 
userLocation:(MKUserLocation *)userLocation
{
    NSLog(@"need to resize");
    CLLocationCoordinate2D destinationCoordinate = 
        destination.placemark.location.coordinate;
    CLLocationCoordinate2D userCoordinate = 
        userLocation.location.coordinate;
 
    double scaleFactor = 2;
    CLLocationDegrees latitudeDelta = 
        (userCoordinate.latitude - 
        destinationCoordinate.latitude)*scaleFactor;
    CLLocationDegrees longitudeDelta = 
        (userCoordinate.longitude - 
        destinationCoordinate.longitude) *scaleFactor;
 
    if (latitudeDelta < 0) {
        latitudeDelta = latitudeDelta * -1;
    }
    if (longitudeDelta < 0) {
        longitudeDelta = longitudeDelta * -1;
    }
 
    MKCoordinateSpan span = 
    MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
    CLLocationDegrees averageLatitude = 
        (userCoordinate.latitude + 
        destinationCoordinate.latitude)/2;
    CLLocationDegrees averageLongitude = 
        (userCoordinate.longitude + 
        destinationCoordinate.longitude)/2;
    CLLocationCoordinate2D centerCoordinate = 
    CLLocationCoordinate2DMake(averageLatitude, 
        averageLongitude);

    MKCoordinateRegion region = 
    MKCoordinateRegionMake(centerCoordinate, span);

    self.adjustedRegion = [self.routeMap regionThatFits:region];
    [self.routeMap setRegion:self.adjustedRegion animated:YES];
}

The cool part about this code is that it doesn’t care if it’s zooming in or out. It just zooms perfectly to the right region. I had to play with the scalingFactor a couple times to get it to look right. 2 seems to work pretty well.

Back to list

9. Intelligently choose walking vs driving directions

To do this, I needed to get the estimated time for walking directions.

A note on directions. If the MKDirectionRequest has the transportationType set to walking and the destination is not walkable, then the service will return no results. That’s why the transportation type is defaulted to any.

MKDirectionsRequest *request = [[MKDirectionsRequest alloc] init];
 
request.source = [MKMapItem mapItemForCurrentLocation];
request.destination = self.destination;
request.requestsAlternateRoutes = NO;
request.transportType = MKDirectionsTransportTypeWalking;
 
MKDirections *estimatedTimeOfArrival = [[MKDirections alloc] initWithRequest:request];
 
[estimatedTimeOfArrival calculateETAWithCompletionHandler:^(MKETAResponse *response, NSError *error) {
    //do something with the response
}];

After I knew that there was no error, I would check to see if the response.expectedTravelTime was greater than 15 minutes (how much I’d be willing to walk). If it was, I would change the MKDirectionsRequest’s transportType to include driving and find the directions with the modified request.

With the asynchronous call within the completion block, this might be a good candidate for wrapping the asynchronous calls using promises.

Back to list

10. Make the starting region of the route, the same as that of the previous view

Pretty simple. Just pass the region from the first MKMapView to the search results view controller to the RouteViewController through the prepare for segues.

Back to list

At the end of this process, I had an app that I could actually show people and it feels really good.

Up next squashing bugs.

Playing with iOS MapKit Part 2: Reverse Geocoding and Custom Annotation Callouts

Playing With MapKit

Part 1 MapKit Tutorials 

Part 2 Reverse Geocoding and Custom Annotation Callouts

Part 3 Making it Pretty

Part 4 Race Conditions, Other Bugs, and Wrap-up

After following the tutorials in part 1, I wanted to keep exploring MapKit. As reverse geocoding was my next topic, I wanted to show the address of the current location in a callout (view that pops up above the pin).

To do this, I’ll need:

  1. A way to know when the current location was tapped
  2. A way to know the address of the current location
  3. A way to display the address in a callout

1. How do I know when the current location is tapped?

MKMapViewDelegate has a method called:

- (MKAnnotationView *) mapView:(MKMapView *)mapView 
viewForAnnotation:(id<MKAnnotation>)annotation

that is called when an annotation is tapped.

How do I tell that this annotation is my current location?

Notice that the annotation is any class that conforms to MKAnnotation.

Current location comes in a special class called MKUserLocation so you just need to test if the annotation is that class.

if ([annotation isKindOfClass:[MKUserLocation class]]
) {
...
}

Within this if statement, I need to turn the currentLocation into an address.

This MKUserLocation has a CLLocation property, a BOOL property for whether it’s updating, a CLHeading property, a title and a subtitle.

2. How do I find the address with coordinates? (Reverse Geocoding)

Apple provides a CLGeocoder class that has the method

- (void)reverseGeocodeLocation:(CLLocation *)location 
completionHandler:(CLGeocodeCompletionHandler)
completionHandler

All you need is the location which comes from MKUserLocation’s location property.

What does the completion handler return?

NSArray *placemarks, NSError *error

placemarks is an NSArray of CLPlacemarks. Each placemark has an addressDictionary. The key @”FormattedAddressLines” returns an array of formatted address lines.

Typically, there are three lines. One for address, another for city, state, and zip code, and one more for the country. In some situations, there are four lines where the first is the name of a venue.

3. How do I display the address?

One way is to set the callout for the annotation is to set the title of the annotation to the text you want in the callout. For example:

self.address = [NSString stringWithFormat:@" %@\n %@\n 
%@", street, cityState, country];
[(MKUserLocation *)annotation setTitle:self.address];

When I do this, the problem is that there is only one line and the rest of the address gets cut off.

Cut Off Callout

Adding line breaks to the title did not make it appear on additional lines, but only displayed the first line instead.

Callout title with line breaks

Messing with the left and right accessory buttons did not help either.

Callout with left and right accessory views colored red and blue
Callout with left and right accessory views colored red and blue

With no way to set more than two lines of text, I had to make a custom MKPinAnnotationView.

How do I make a custom annotation callout?

Since the callout is a property of the MKPinAnnotationView, I had to make a subclass of the MKPinAnnotationView and overwrite the setSelected: animated method.

- (void) setSelected:(BOOL)selected animated:(BOOL)animated
  1. Draw a label
  2. Set the text of the label with the annotation’s title
  3. Place a view as a subview of the MKPinAnnotationView subclass (which I called MultilineAnnotationView)
  4. Add the label as a subview of view.
setSelected
Why MKPinAnnotationView and not the default user location view?

The default user location view is a class that you cannot access or subclass.

Custom Callout View
The custom callout works! (but not perfectly)

Up next, making things look pretty.

Playing with iOS MapKit Part 1: MapKit Tutorials

Playing With MapKit

Part 1 MapKit Tutorials

Part 2 Reverse Geocoding and Custom Annotation Callouts

Part 3 Making it Pretty

Part 4 Race Conditions, Other Bugs, and Wrap-up

A main difference between a smartphone and any other phone is that smart phones can tell you where they are. What does location tell you? Location provides context about what you are doing, where you’re going, and by extension, who you are.

Apple makes it easy to show this information on a map with MapKit.

At the Flatiron School, I got a good foundation for learning frameworks, but hadn’t worked with MapKit yet. Since it felt core to many mobile apps, I decided to explore it.

Demo Video

Here is a demo of the finished app. All the code is on Github.

First Stop on the MapKit Train: Tutorials

Whenever I get into a new framework, I learn best by jumping right in and  doing it. Tutorials are a great way to get started. They help me focus by limiting scope and stay productive before my natural curiosity wants to go down some rabbit holes. I also don’t want to reinvent the wheel.

If I have seen further it is by standing on the shoulders of giants.

-Isaac Newton

Searching for iOS Mapkit tutorial, I find some by Ray Wenderlich, TechTopia, and AppCoda. All three have been really useful in the past. I went with the TechTopia one because it was written about iOS 7.

Tutorial 1: Make a MKMapView and Show the Current Location

Basically:

  • Add the MapKit framework
  • Make a MKMapView with Storyboards and import MKMapView
  • Assign the MKMapViewDelegate to the view controller
  • Set the MKMapView to show the current Location
  • *Not In Tutorial: Asking for Permission to Access User Location
  • *Set the location in Simulator
  • Zoom in on the MKMapView by changing the region
  • Change the MKMapView type
  • Update the MKMapView using the MKMapViewDelegate methods

*Most of this is already well explained in the tutorial. I want to point out two bumps on the road.

1. Simulator does not have a GPS, so you have to set the location by the menu bar: Debug -> Location -> Custom Location.

Google maps will give you latitude and longitude for any location if you click on the map. If it’s an icon, right click and click “What’s here?”.

Note: Sometimes the location doesn’t show up in the simulator the first time I run it. When I run it on another sized simulator, it works. Don’t know why this is.

2. More importantly, iOS 8 requires that you get permission to use the user location.

How to Get the Current Location Authorization Status

    CLAuthorizationStatus status = [CLLocationManager 
authorizationStatus];

CLAuthorizationStatus is an enum of

  • kCLAuthorizationStatusNotDetermined = 0
  • kCLAuthorizationStatusRestricted = 1
  • kCLAuthorizationStatusDenied = 2
  • kCLAuthorizationStatusAuthorized = 3 (Deprecated iOS 8)
  • kCLAuthorizationStatusAuthorizedAlways = kCLAuthorizationStatusAuthorized = 4
  • kCLAuthorizationStatusAuthorizedWhenInUse = 5

Statuses start out as kCLAuthorizationStatusNotDetermined

How to Ask the User for Location Authorization

If you want current location data only when the customer is using the app:

[self.locationManager requestWhenInUseAuthorization];

If you want current location data even when the customer is not using the app:

[self.locationManager requestAlwaysAuthorization];

Here I have my CLLocationManager as a property of the class. Change self.locationManager to the name of your locationManager.

How to Add Properties to your Info.plist

Even though you think you’ve requested authorization, you are likely still missing one more piece.

Properties List

In the Supporting Files Folder of your app directory, there is a Info.plist file.

It’s a properties list that your app uses through out the app.

You have to have a property in there for NSLocationWhenInUseUsageDescription (or NSLocationAlwaysUsageDescription depending on which permission your asking for), which tells the customer why you are asking for their location information.

Permission Popup

How to tell if you received location authorization

There is a delegate method for the location manager that tell you when the authorization status changes.

- (void)locationManager:(CLLocationManager *)manager 
didChangeAuthorizationStatus:(CLAuthorizationStatus)
status

The status here is the CLAuthorizationStatus.

Remember to set the locationManager’s delegate to the class that implements the delegate method. In this case, I’ve set the delegate to self inside my view controller.

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

Tutorial 2: Add Local Search with MKLocalSearchRequest and Display Results as MKPointAnnotation

Basically,

  • Add a textField so the user can input search terms
  • Add an IB action for the textFieldReturn to call the performSearch method
  • Add a performSearch method that sends a MKLocalSearchRequest and handles the MKLocalSearchResponse by parsing its array of MKMapItems into MKPointAnnotations

This was pretty straight forward. Apple makes the searches super easy by giving you completion handlers.

How to Perform a Local Search

You just need a MKLocalSearch object and a MKLocalSearchRequest object.

MKLocalSearch *search = [[MKLocalSearch alloc] 
initWithRequest:request];

and call

- (void)startWithCompletionHandler: 
(MKLocalSearchCompletionHandler)completionHandler

Tutorial 3: Find Directions with MKDirectionsRequest and Draw Them on the Map

Basically,

  • Add a ResultsTableViewController to show the names and phone numbers of the venues
  • Add a RouteViewController to show the route to the destination
  • Set up a MKDirectionsRequest with a source and destination in the RouteViewController
  • Make a MKDirections instance that calculates directions
  • Pass the MKDirectionsResponse to a showRoute method that adds an overlay of the polylines of the MKRoutes in the response to the map
  • Set up how the overlay will look

Notes

The tutorial sets up a custom UITableViewCell, but that’s not really necessary because you can set the title as the name and the subtitle to the phoneNumber.

Why am I in the middle of the ocean?

It’s important that you implement the MKMapView delegate method

- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:
(MKUserLocation *)userLocation

Otherwise, you’ll find yourself in the ocean next to West Africa a lot (that’s what happens when your coordinates are (0,0)).

How to Get Directions

Make an instance of MKDirections and call:

- (void)calculateDirectionsWithCompletionHandler:
(MKDirectionsHandler)completionHandler

Up next: Going beyond the tutorials

Flatiron School Presents – The Multipeer Connectivity Framework (Lattice)

Another Flatiron student and I were interested in the Multipeer Connectivity Framework in iOS, so we made a chat app called Lattice that uses multipeer in the event of a natural disaster as a demonstration of the technology.

I’m going to talk about how the basics of multipeer connectivity work.

The Multipeer Connectivity (MC) Framework

What does it do?

Multipeer allows devices to connect with others nearby without the internet.

How does it work?

Multipeer Connectivity is an implementation of mesh networking, creating a local network between devices within Wifi or Bluetooth range. Not only are new devices connected with devices within range but also with all devices that those devices are connected to, even if the destination device is not within range of the sender.

That’s the real power of mesh networks. If permitted, devices can send messages relayed through other devices.

How might this be useful?

  1. Mesh networks allow communication when internet infrastructure is damaged (such as natural disasters).
  2. Mesh networks allow communication when internet access is restricted by governments, seen in the protests of Hong Kong.
  3. Mesh networks allow communication in internet-less situation (camping trips, road trips, subway commutes, and foreign vacations).

Lattice – Demo Project

Before we dive into the nitty gritty, I’ve placed Lattice on Github, so you can see exactly how the snippets below are used.


Setting up the MCManager

We have a MCManager class, similar to an API manager or data store for Core Data. Four properties need to be set up.

MCPeerID

_peerID = [[MCPeerID alloc] initWithDisplayName:displayName];

This is the name of your device.

MCSession

_session = [[MCSession alloc] initWithPeer:self.peerID securityIdentity:nil encryptionPreference:MCEncryptionNone];
_session.delegate = self;

This is the session that your device will start. Think of a session like a chatroom.

MCNearbyServiceAdvertiser

_advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:self.peerID discoveryInfo:elements serviceType:LatticeServiceType];
self.advertiser.delegate = self;
[self.advertiser startAdvertisingPeer];

This adverser tells nearby browsers that it is looking to connect with a particular serviceType. DiscoveryInfo is a dictionary that the browser will receive when it finds this advertiser.

MCNearbyServiceBrowser

_browser = [[MCNearbyServiceBrowser alloc] initWithPeer:self.peerID serviceType:LatticeServiceType];
self.browser.delegate = self;
[self.browser startBrowsingForPeers];

This browser looks for advertisers of a particular service type to connect to.


Connecting to Other Devices

Lattice, the demo app, will have an advertiser and browser running at the same time on each device.

Browser Finds Peer, Invites Peer

- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
{
   [self.browser invitePeer:peerID toSession:self.session withContext:nil timeout:10]
}

When a browser finds an advertiser, it can parse the discoveryInfo that the advertiser set when it was created. In this case, we want the devices to connect automatically, so the browser sends an invitation to connect to the browser’s session.

Advertiser Accepts Invitation

- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL, MCSession *))invitationHandler
{ 
    invitationHandler(YES, self.session); 
}

Since we want the advertiser to connect automatically, the advertiser will reply YES to the invitation.

Session Confirms Connection

- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state

We can indeed confirm that the two devices are connected with this delegate method from the session. The session will tells us that the peer is now in the connected state.


Sending and Receiving Messages

Sending From the View Controller

[self.multipeerManager.session sendData:messageData toPeers:self.multipeerManager.session.connectedPeers withMode:MCSessionSendDataUnreliable error:&error];

Messages are sent from the session to the peers in the session.

Receiving In the MCManager

- (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID
{
    //Kicks off a notification to the View Controller 
}

Messages are received by the session on the other devices. In Lattice, we triggered a notification with each message received.

Catching the Notification In the View Controller

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(peerDidReceiveDataWithNotification:) name:@"MCDidReceiveDataNotification" object:nil];
- (void)peerDidReceiveDataWithNotification:(NSNotification *)notification
{
    NSData *messageData = notification.userInfo[@"data"];
    NSArray *messages = [NSKeyedUnarchiver unarchiveObjectWithData:messageData];
    [self.demoData.messages addObject:messages[0]];
}

The notification was handled by the view controller to display the new message.


Limitations

iOS only

Multipeer Connectivity is a iOS framework.

Range can be limited on Bluetooth

We found it to be about 60 – 100 feet.

Downloading the app requires the internet?!

There should be a way for the app to spread virally with no internet.

Limited usefulness in zombie apocalypse

This would be great for finding survivors nearby without having to go room by room through scary dark buildings. During night when the zombies are outside, the bluetooth signal may not be long enough to communicate between buildings.


Special Thanks

Peter Fennema for showing how to connect via multipeer automatically

Jesse Squires for the messaging user interface: JSQMessagesViewController

Flatiron School Week 9 – Conquering Fear of Complex CocoaPods

For my Flatiron School Presents project, a classmate and I worked on a chat app.

For the chat user interface, I wanted it to look like Apple’s Messages app. There was a perfect CocoaPod for this: JSQMessagesViewController. After reading the Getting Started section, I still had no clue how to bridge the gap between my existing app and this UI. It was overwhelming and I was very discouraged.

9 Steps to Overcoming Fear of Complex CocoaPods

Step 1: Recognizing the problem

I knew that I was avoiding it. I looked for another CocoaPod but they weren’t quite the same. I even thought of building my own chat UI. I realized that I needed to be able to use CocoaPods though, so there was no getting around it.

Step 2: Read Zen Pencils

In need of inspiration, I turned to the Zen Pencils. I have the book too, which makes for very motivating reading.

Step 3: Go to sleep

It was 9 already and so I figured it was better to get some sleep than try that night.

Step 4: Try a simpler CocoaPod.

I hadn’t used enough CocoaPods up to this point, so I tried a smaller one. I looked at HPL chat. It had a tenth as many classes as JSQ did and I went for it. It didn’t work, but got me started.

Step 5: Look at the demo

The challenge of open source software is that there’s not always good documentation. It doesn’t cost money, but it costs time to read and understand how to use the code.

Fortunately, I saw that JSQ had a demo and downloaded it. Playing around with the demo really helped me understand how the pieces related to each other.

Step 6: Start customizing the demo

I didn’t need images or videos so, I got rid of the accessory button on the toolbar. I had to get familiar with the documentation to find which parts I needed to change. Once I removed the button, I felt much more confident about adapting the demo.

Step 7: Move the demo to a new project and make it work

Now I tried to make the demo fit my purpose, removing as much as I could to reduce it to the elements I needed. Here was a sandbox that I could use to unit test the functionality before I put it into my app and get everything tangled up.

Step 8. Add to the app

With the chat working on it’s own, I had to hook it up to my project. By this point, I realized that I just needed to make a couple  subclasses of the classes that the CocoaPod needed and replace my existing messages class with those.

Step 9. Fear Conquered

After I was done, I read the CocoaPod’s Getting Started again and now it all makes sense. Going through implementing this pod has been a process of gradual building of exposure, comfort and confidence with CocoaPods. I’m really happy that I did this. Fear of technology really comes down to not understanding how it works.

Flatiron School Week 9 – Resolving my first real git merge conflict

This week we started our client projects. 19 people broke down into teams of 4 or 5. There were four clients and our team had four people. This was the first time that I really needed to deal with git merge conflicts and develop a workflow.

It wasn’t long before I had a conflict.

There were three files that I conflicts with:

  1. AppDelegate.m
  2. the .pbxproj file
  3. a xccheckout file

1. AppDelegate.m

I knew how to resolve the first one, but as I tried to do that, my file directory on the left side of Xcode disappeared. It was time to ask for help. Zach, one of our TAs, helped me put together a plan to resolve the conflicts.

I had to back out of the merge, so I used

git merge --abort

and then reset the head to the last commit because there some uncommit changes that I didn’t want anymore.

git reset --hard <commit id>

2. the .pbxproj file

Apparently that file directory is stored in the .pbxproj file. The way that we resolved this was by telling git to just join the two versions together in the .gitattributes file by adding:

*.pbxproj merge=union

Another way to resolve this is to choose one version of the pbxproj.

git checkout --ours <directory/file>

Replace “ours” with “theirs” to get the version being merged into the current branch.

I had to add in some files that were no longer referenced by the pbxproj file after the merge using this second method.

3. a xccheckout file

For the .xccheckout file, this was a file that should have been ignored. I added this to the .gitignore file.

*.xccheckout

The problem was that both versions that I was trying to merge still had this file in tracked files. I had to remove this file from both repos by running:

git rm --cached <directory/file>

Once the .xccheckout file was removed, I could merge the two repos.

Both the .gitignore and .gitattributes files should be set up at the beginning of a project.

Update

Since that week, we’ve had to resolve conflicts many times. Following Github’s workflow has been pretty useful. I’ve found that the best way to resolve conflicts is prevention. We designed our workflow so that each person worked on a different part of the app and that minimized conflicts.

Flatiron School Week 7 – BocceGame built with UIDynamicAnimator

UIKit Dynamics

In my spare time, I wanted to look at animating physics collisions so I found UI Dynamics. It’s a part of UIKit that has some build-in functionality to animate physics based interactions. It’s generally not used for games, but rather for transitions and cool UI tricks.

A bocce game is a simple way to demonstrate this functionality. In bocce, there’s a target ball called the jack and two teams try to get their ball as close to the jack as possible. The team gets points for the number of balls that are closer to the jack than the nearest opponent ball.

Bocce Game Demo

Github Repo

How to use UIDynamicAnimator

You have an animator.

Animators can add behaviors.

Behaviors can add items that they affect.

My animator

    self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];

One behavior is the UIDynamicItemBehavior behavior that allows the ball to continue moving after released by the hand with the add

self.linearVelocity = [[UIDynamicItemBehavior alloc] init];
[self.linearVelocity addLinearVelocity:velocity forItem:gesture.view];
[self.animator addBehavior:self.linearVelocity];

Another behavior is the collision behavior that collides with either a wall or another item.

//initialize collision behavior
self.collision = [[UICollisionBehavior alloc] init];
[self.animator addBehavior:self.collision];

How to make this game

Dragging a ball

Set up a UIPanGestureRecognizer that calls the dragged: method.

    UIPanGestureRecognizer *draggable = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragged:)];
    [self.currentBlock addGestureRecognizer:draggable];

The dragged method has the form:

- (void) dragged:(UIPanGestureRecognizer *)gesture
{
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
//save the starting location
break;

case UIGestureRecognizerStateChanged:
{
CGPoint translation = [gesture translationInView:self.view];
 
//calculate the new location with the starting location and the translation
//set the location to the new location
 
break;
}
 
case UIGestureRecognizerStateEnded:
{
 
//figure out the velocity when the ball is released and add it to the item with the UIDynamicItemBehavior
}
 
break;
}
 
default:
break;
}

}
}

Continue a ball moving after release

Right now, the ball would stop when I let go of it. To keep it going at the same speed, get the velocity in view from the gestureRecognizer and add it to the UIDynamicItemBehavior with addLinearVelocity: forItem:

//Inside the case UIGestureRecognizerStateEnded
CGPoint velocity = [gesture velocityInView:self.view];
[self.linearVelocity addLinearVelocity:velocity forItem:gesture.view];
[self.animator addBehavior:self.linearVelocity];
 
//necessary to make the motion smooth, otherwise it will be jumpy if you hold down the 
[self.animator updateItemUsingCurrentState:gesture.view];

Stopping the ball

The ball will keep going on for a long time, so I needed to stop it somehow.

Stop the ball by adding friction. In UIDynamicItemBehavior, friction is the resistive force between two objects. I wanted resistance to simulate air resistance. Angular resistance was needed to stop the balls from spinning too.

self.linearVelocity.resistance = 8;
self.linearVelocity.angularResistance = 8;

Only allow one animation to run at a time

I found that trying to flick a ball before another had finished moving was making a really jumpy animation, so checked if the animation is running with each gesture recognized and only let the new block move after the other was finished moving.

- (void) dragged:(UIPanGestureRecognizer *)gesture
{
    if (![self.animator isRunning]) {
        switch (gesture.state) {
...

Detecting when the ball was stopped

To find out when to make the next block, I needed to know when the ball had stopped moving. I used the

- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator

method. It’s a delegate method so I had to set the

    self.animator.delegate = self;

Making the target ball bounce off the walls

I didn’t want the target ball to go off the screen, so I set the screen boundaries as walls,

[self.collision setTranslatesReferenceBoundsIntoBoundary:YES];

made the ball bound off all objects including boundaries (referenceBounds being those of self.view because the animator has self.view as the reference view),

[self.collision setCollisionMode:UICollisionBehaviorModeEverything];

and added another invisible wall, so that the ball would not end up on the top edge of the screen.

[self.collision addBoundaryWithIdentifier:@"end" fromPoint:CGPointMake(0, self.view.frame.size.height*0.1) toPoint:CGPointMake(self.view.frame.size.width, self.view.frame.size.height*0.1)];

Take the target ball out of collision

I wanted to make the target ball immovable, so I just had to remove it as an item of the collision.

[self.collision removeItem:self.currentBlock];

I also wanted future object to be able to go off the screen, so I removed the “end” boundary and the window boundaries.

[self.collision removeBoundaryWithIdentifier:@"end"];
[self.collision setTranslatesReferenceBoundsIntoBoundary:NO];

Ignoring most the call to dynamicAnimatorDidPause after a ball is made

After a new object is added, dynamicAnimatorDidPause is called, so I had to add a BOOL property justMadeBlock to ignore those calls.

Stop the moving block when it goes offscreen

If a ball was flicked too hard, then it would go off the screen and I would have to wait for it to stop before the next ball would load. To shorten that time, I made another boundary outside the edge of the screen and overwrote a delegate method to make the ball stop moving and not able to collide with other blocks.

[self.collision addBoundaryWithIdentifier:@"outOfBounds" forPath:[UIBezierPath bezierPathWithRect:CGRectMake(-90, -90, self.view.frame.size.width+180, self.view.frame.size.height+180)]];

Delegate method for the UICollisionBehavor

self.collision.collisionDelegate = self;
- (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id<UIDynamicItem>)item withBoundaryIdentifier:(id<NSCopying>)identifier atPoint:(CGPoint)p
{
    if ([((NSString *)identifier) isEqualToString:@"outOfBounds"]) {
        //not allow it to move
        [self.linearVelocity removeItem:item];
        //not allow it to interact with future blocks
        [self.collision removeItem:item];
    }
}

Calculate the nearest ball and marking the nearest ones

Once a colored ball had been thrown, there is now a winning ball (closest to the target).

I took all the balls on screen.

    NSArray *blocksOnScreen = [self.collision items];

Got their locations

Sorted the array of balls by location

And found the balls of the same color that were closest to the target ball

With those “winning balls”, I added a CALayer to them to mark them as winning.

The next to go would be the team further away from the target.

At the end of the game, a UIAlertController asks if you want to play again.

Adding indicators for the number of balls remaining per team

I used CAShapeLayers and added them to the self.view.layer. They would update as balls were made.

Adding a flashing turnIndicator

The alpha controls the opacity of the text label, so setAlpha:1.0 means make it appear.

UIViewAnimationOptionAutoreverse makes it disappear again.
UIViewAnimationOptionRepeat makes the animation keep going until you stop it.
UIViewAnimationOptionAllowUserInteraction allows you to remove the animation later on.

[UIView animateWithDuration:1.5 delay:0 options:UIViewAnimationOptionAutoreverse| UIViewAnimationOptionRepeat | UIViewAnimationOptionAllowUserInteraction  animations:^{
        [self.turnIndicator setAlpha:1.0];
    } completion:nil];

Stopping animations

When I tried to reset the game, setting all the properties back to nil worked well, except for stopping the turnIndicator animation.

There I found that the animation is in the layer of the animated object.

[self.turnIndicator.layer removeAllAnimations];

Choosing a more attractive color scheme

Adobe’s color wheel was really cool. I like pastel colors, so this is what I chose.

Notes to Self

Writing code takes way longer than expected

I didn’t think that it would take very long to make this, (just slap some blocks on a screen right?) but building features takes time and focus. Really quickly, seeing jumpy animations makes you want to fix them and that’s the 20% that takes the most time. Writing blogs about code also takes hours…

User testing is super important to do throughout

I knew how the game was played, but it wasn’t obvious to people I showed it to. I really wouldn’t have known if I hadn’t had someone use the game.

Don’t let sidetracked on super advanced features when the basics are not done

The flip side of asking for feedback is that people start giving you really good suggestions, but I think you have to finish one phase before hacking away at the next thing.

Hacking is fun with something quick, but it’s no fun with a big project

This is the second project that I’ve hacked to together. It’s been fun, but I really would have liked to have classes for the data model. This game was made over three weekends and I was finding that when I wanted to add new features, I had to recall the mental model that I had for the project. Instead I should have had a game class that remembered the model for me.

Flatiron School Week 4- Hacking together the TinderClone Swipe Feature

The Tinder Swipe

The central part of Tinder’s design is the swiping mechanism. The feature allows people to focus on one match at a time and make a decision before moving on to the next match.  Good for spending more time on matches and good for reducing the speed of data consumption.

I wanted to learn about animations, so this weekend I hacked together a prototype of the feature.

Demo

All the code can be found here along with a video demo. The code is very dirty, but that’s topic for another week.

Process

Step 1. Look up how to do it.

The next step in my TinderClone project is to make the swipeable images. Richard Kim has a really cool blog entry about how to make this feature three different ways. Since I’m taking on this project to learn to code, I took the “build it from scratch” way and tried to follow Nimrod Gutman’s blog. I totally failed. Twice.

It turns out that I didn’t really understand how UIView and UIImageViews worked. So, I decided to play around with them.

Step 2. Read up on UIViews

I read thought through the Apple View Programming Guide. in particular, the view and window architecture. I didn’t really understand bounds very well, so I started an Xcode project called playingWithBounds, that ended up being my project.

Take-aways:

  • Views are made on top of Core Animation layers (CALayer). Views have animation, but go to CALayer to one layer deeper for more control.
  • Subviews are arranged in an array in the superview.
  • UIContentMode is useful for sizing images. I played around with this later.
  • I thought the bounds property might have something to do with Box Sizing in CSS, but it’s much simpler. The bounds change the frame of the box.

UIView properties

  • frame- used for making objects. CGRectMake()
  • bounds- changes the size of the view, but not the center.
  • center- very important for moving objects around.
  • transform- used to rotate, move and scale objects. It’s the finishing touch. The object is drawn and then transformed. When an object is rotated, it remembers what it’s heigh and width were when it wasn’t rotated.
  • alpha- 1 is opaque and 0 is fully transparent. Great for
  • backgroundColor- useful for seeing where the rectangle is.

Step 3. Playing around with the UIView properties

I made single view project where I drew boxes and changed the bounds, and so on to see what would happen.

Step 4. Playing around with CGAffineTranform

When I got to Coordinate System Transformations, I started playing around with rotating and moving boxes.

I made a slider to rotate an UIImageView.

//slider
        CGRect slideFrame = CGRectMake(100, 400, 200, 20);
        self.pictureRotationAngle = [[UISlider alloc] initWithFrame:slideFrame];
        [self.view addSubview:self.pictureRotationAngle];
        self.pictureRotationAngle.minimumValue = 0;
        self.pictureRotationAngle.maximumValue = 2*M_PI;
        self.pictureRotationAngle.value = M_PI/2.0;
 
        [self.pictureRotationAngle addTarget:self action:@selector(rotateImage) forControlEvents:UIControlEventValueChanged];
 
        self.picture.transform = CGAffineTransformMakeRotation(self.pictureRotationAngle.value);

Step 5. Finding the Event Handling methods

Where other solutions mentioned at the top used UIGestureRecognizers, I imagined that these event handling methods could move my picture around too. They used UITouch objects.

I was touching my UIImageView and nothing was happening. Googling around, I found that UIImageViews are not user interactive by default. So, I had to set it.

    self.picture.userInteractionEnabled = YES;

I also set the

What is UITouch?

I didn’t know, so I built the first three methods and NSLogged the touch and event objects. They were very descriptive.

2014-10-27 18:58:26.919 TinderSwipeFeature[65292:2410541] <UITouch: 0x7fdf015533a0> phase: Moved tap count: 1 window: <UIWindow: 0x7fdf01409040; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x7fdf01403730>; layer = <UIWindowLayer: 0x7fdf01414c50>> view: <UIImageView: 0x7fdf01684780; frame = (37.8671 64.9702; 402.124 402.124); transform = [0.99420459268393058, -0.10750454821160646, 0.10750454821160646, 0.99420459268393058, 0, 0]; clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x7fdf01684ac0>> location in window: {256, 332} previous location in window: {255, 332} location in view: {192.38019379786252, 249.92073845470227} previous location in view: {191.38598920517859, 249.81323390649064}

UITouch has a lot of really useful info:

  • Size of the screen: 375 width and 667 points height.
    frame = (0 0; 375 667)
  • The view touched and it’s location
    view: <UIImageView: 0x7fdf01684780; frame = (37.8671 64.9702; 402.124 402.124)
  • Current touch location in view and window
    location in window: {256, 332} 
    location in view: {192.38019379786252, 249.92073845470227}
  • Previous touch location in view and window
    previous location in window: {255, 332}
    previous location in view: {191.38598920517859, 249.81323390649064}
  • Timestamp
    2014-10-27 18:58:26.919

I now had the velocity of touches and I just needed to update the view being touched with a new center value to move it.

That was the key to solving the problem.

I added the other Tinder style animations associated with swiping. All of these were based on using the x location of the touch divided by the width of the phone to calculate a factor that would modify the rotation of the image and the opacity of the overlaps.

Step 6. Playing with contentMode

When I put the picture in the UIImageView, it would not fit in the frame, so I had to set clipsToBounds to YES and change the contentMode from UIViewContentModeScaleToFill to UIViewContentModeScaleAspectFill. UIViewContentModeScaleAspectFit also works but you get white space on the sides.

    self.picture.contentMode = UIViewContentModeScaleAspectFill;
    self.picture.clipsToBounds = YES;

The other options such as UIViewContentModeTop maintain the size of the image, and align the listed edge to the corresponding edge in the box. For example,

  • UIViewContentModeTop aligns the image to the top of the UIImageView.
  • UIViewContentModeTopLeft aligns the image to the top and left of the UIImageView.

Step 7. Animation timing

For the touchesEnded method, I wanted to animate the image moving back into location.

[UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.6 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
                ... reset image
            } completion:nil];

CGAffineTransformIdentity removes all transforms (rotations, scaling, translations).

Step 8. Animation for the picture flying off the screen or springing back

To animate the picture flying off to the side, I set a swipe threshold of 30% of the width.

if (differenceInTouchLocationX > aTouch.window.frame.size.width*0.2) {
            [UIView animateWithDuration:0.5 animations:^{
                viewTouched.center = CGPointMake(self.view.frame.size.width*2, currentLocation.y + 0.5 * speedY) ;
 
            }];
            NSLog(@"%@", aTouch);
        }else if (differenceInTouchLocationX < -aTouch.window.frame.size.width*0.3)
        {
            [UIView animateWithDuration:0.5 animations:^{
                viewTouched.center = CGPointMake(-self.view.frame.size.width*2, currentLocation.y + 0.5 * speedY) ;
            }];
        }
        else { 
             ... //springs back to original location
        }

Step 9. Add the overlap images (Like and Nope)

self.like = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 100, 50)];
 [self.like setFont:[UIFont systemFontOfSize:100]];
 self.like.text = @"L I K E";
 self.like.adjustsFontSizeToFitWidth = YES;
 [self.like setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
 [self.like setTextColor:[UIColor colorWithRed:47/255.0f green:156/255.0f blue:28/255.0f alpha:1]];
// self.like.backgroundColor = [UIColor blueColor];
 self.like.transform = CGAffineTransformMakeRotation(-20* M_PI/180);
 self.like.alpha = 0;

Boy, I learned a lot by just reading the documentation and Googling. That was fun!

Week 2- Tinder Clone. Breaking down into Objects. Setting up the PageViewController

I felt that I had enough knowledge to start building apps so I decided to copy some popular apps with interesting mechanisms. The idea is to do one every week or two, so that I can build a lot of these apps and learn how they are built.

Why build lots of apps?

Part of my inspiration has been Ira Glass’s notes on storytelling where he walks about beginners needing to do a great volume of work before they can make things that they think are good enough. I’m starting that journey now.

Why reverse engineer apps and not build original apps?

When artists learn to paint, they copy other artists to learn how something is done. In app making, I’m doing the same by analyzing and appreciating how other developers have made their apps.

Project 1 is Tinder.

The interesting mechanism here is the swipe left or right to like or dislike.

Step 1 Break down the app into views

Sketch of Tinder Settings Page
Sketch of Tinder Settings Page
Sketch of Tinder Match Page
Sketch of Tinder Match Page
Sketch of Tinder Messages Page
Sketch of Tinder Messages Page

For my first iteration, I really just want the settings and the match page to work. The messages would be part of the second iteration and moments would be part of the third.

Step 2. Take the views and make the data model

I started by translating the Settings and Match views into classes and properties, leaving the methods fluid for now.

There needs to be a Person class.

Person Class
Person Class

The Facebook properties would be imported with Facebook authentication and the rest would be set by the customer.

For simplicity, I have the location as a NSString now.

When I thought about how the custom initializer would work, I realized that I would probably need to split this class up into small modules with Discovery Properties, and Facebook Properties as classes.

I realized that I would also need a way to match people. It made more sense to make a Matcher class because I would want a central place to store data and review the results (whether people liked each other) to see how well my matching algorithm was working.

MatchMaker Class
MatchMaker Class

Naturally, the array of matches would need a match object, so I wrote a match class.

Match Class
Match Class

Step 3. Make the basic interface in storyboard

Tinder has four pages but I just wanted to mock up the Settings, Match and Messages pages.

Specifications:

  • Slides horizontally from page to page with Settings, Match, and Messages page from left to right.
  • Starts on the Match Page

1st Iteration

Test: I tried three UIViewControllers with buttons on navigation bars that went between the three pages.

  1. Select the storyboard or make one (File > New > File Cmd-N, iOS > User Interface > Storyboard)
  2. Add three View Controllers from the bottom of the Utilities column (right sidebar).
  3. Add a navigation bar to each of the three view controllers.
  4. Add bar button items to each screen as shown below.
  5. Add segues for the bar buttons to the adjacent screens.
1st Iteration of the Storyboard
1st Iteration of the Storyboard

Results: As I found out after running this, the transitions make the new screen pop up from the bottom like a modal window.

  • Slides horizontally from page to page with Settings, Match, and Messages page from left to right. 
  • Starts on the Match Page

2nd Iteration

Test: Use a navigation controller.

I have a navigation controller with the root controller as the Settings page with the Match page and the Messages Page linked to that.

2nd Iteration of the Storyboard
2nd Iteration of the Storyboard using a Navigation Controller

Results: The transitions are now horizontal between pages. It’s not smooth like Tinder’s touch gestures, but it’s close. The app doesn’t start on the match page though if I want the sliding to work in this order.

  • Slides horizontally from page to page with Settings, Match, and Messages page from left to right. 
  • Starts on the Match Page

3rd Iteration

Test: Make the Match page the root view controller and add a link back to the settings page.

3rd Iteration of the Storyboard
3rd Iteration of the Storyboard

Results: After clicking the settings button, the transition slid from right to left and the Settings pages looks just like the messages page. Not what I wanted.

  • Slides horizontally from page to page with Settings, Match, and Messages page from left to right. 
  • Starts on the Match Page

4th Iteration

Test: I’m guessing that the transition is either a customer segue or there’s something about gestures that I don’t know. I’m going to look into gestures between views.

As I looked through the gestures, I noticed the Page View Controller. After googling it and finding this AppCoda tutorial,  the scroll transition looks exactly like what Tinder uses.

5th Iteration

A Page View Controller is a container controller that you put other view controllers inside.

So, I’m going to need to make three classes of view controllers that show the Settings, Match and Messages pages. Then, I’ll put them inside the page view controller.

Looking through the PageViewDemo from the AppCoda tutorial, I see that they have one view controller that creates a pageViewController, provides the data for the pageViewController and sets an array of PageContentViewControllers in the pageViewController.

Sidebar: Navigation Bar vs. Navigation Item

When I was using the Navigation Controller, I needed to add navigation items to the Settings, Match and Message view controllers to be able to add a title to each slide.

When I deleted the Navigation Controller, I made the navigation bar disappear. While the navigation item was still there, it was useless without the navigation bar, so I had to add three navigation bars onto the Settings, Match and Message view controllers.

Where a navigation controller exists, add navigation items. Where there is not , add navigation bars. 

Test: In order to implement the pageViewController, I added a ViewController class inheriting from UIViewController and added a PageViewController, a SettingsViewController, MatchViewController, and MessagesViewController into the properties. The last three are custom classes inheriting from UIViewController.

ViewController.h inheriting from UIViewController
ViewController.m inheriting from UIViewController

I created the five view controllers in the Storyboard and added their class name to each corresponding storyboard ID.

5th iteration of the storyboard
5th iteration of the storyboard

Then I set the properties in ViewController.m to each of the storyboard IDs.

ViewController.m
ViewController.m

Here I set the match view controller to be the starting one.

  • Slides horizontally from page to page with Settings, Match, and Messages page from left to right. 
  • Starts on the Match Page

Now it starts with the match page, but doesn’t have any other other pages. The Settings and Messages pages are there, but they are not connected yet.

I see that the PageViewDemo has two methods called

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController

and

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController

That’s probably what I want to tell the page view controller how to switch between the Settings, Match and Messages view controllers.

When I try to type those in, they were not autocompleting, which means that I was missing something. I think it’s because I haven’t set the ViewController to follow the pageViewControllerDataSource Protocol. After I add that to the ViewController.h file. I get a warning. Yes!!

PageViewControllerDataSource Protocol Warning
PageViewControllerDataSource Protocol Warning

Now I understand why the PageViewDemo had

    self.pageViewController.dataSource = self;

in the viewDidLoad.

Since I only had three pages, I wrote two simple if statement for the viewControllerBefore and viewControllerAfter methods and everything works.

  • Slides horizontally from page to page with Settings, Match, and Messages page from left to right. 
  • Starts on the Match Page