22 Sep 2015
Belle

Exist for iOS: creating the weather card

Last weekend I completed the last custom “card” view in the Exist for iOS dashboard. If you’re not up to date, check out what I was up to before I started this blog series in Part 1, and how I added the mood card in Part 2.

The weather card process was fairly similar to when I added the mood card—I already had almost all the data I needed, and again I worked from Josh’s Android app to get the layout right. The majority of my time was spent debugging Auto Layout constraints, as it was with my mood card. I guess I haven’t learned much.

Here's what Josh's weather card looks like in the Android app, which is what I was copying:

Android weather card

Apologies for the lack of images in this post. It would have been helpful to explain the issues I had with constraints, but I forgot to take screenshots while I was working. I’ll try to remember for next time!

Getting the data

I already had almost all the data I needed. For the dashboard I poll the “today” API endpoint, which gives me back the user’s current values for all their attributes.

The one part that was missing was the weather icon, which changes depending on the conditions. Josh uses local image files for the weather icons in his Android app—the same images he used for the Exist web dashboard:

web weather tile

So I grabbed the image files and added them all to my project in Xcode (I’ve never figured out how to do stuff like that in AppCode). We use Forecast.io for weather data, which offers more conditions than we have icons for. For instance, “partly cloudy day” and “partly cloudy night” conditions from Forecast both get a simple “party cloudy” icon in Exist.

This meant I couldn’t match the conditions value directly to an icon name in every instance, so I created a dictionary that included all the conditions and the image name that matches each one. When I’m ready to create the weather icon, I use the dictionary to pull the right image name before adding it to my UIImageView:

NSString *imageNameString = [self.icons valueForKey:[_valuesDict valueForKey:@"weather_icon"]];  
_weatherIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:imageNameString]];

With the images ready to go, I just had to create all the views I needed and lay them out using constraints. The views were pretty simple for this card:

UIView *_view;  
UILabel *_minTempLabel;  
UILabel *_maxTempLabel;  
UILabel *_tempSlashLabel;  
UILabel *_weatherUnitsLabel;  
UILabel *_weatherSummaryLabel;  
UIImageView *_weatherIcon;  
UIView *_weatherTempsView;

To help me figure out the constraints I’ve taken to drawing a wireframe-style image of the layout I want in a notebook. I draw in all the views, then use a different coloured pen to mark which constraints I’ve created, so I can see where I might not have created enough.

constraints drawing

I didn’t have any ambigious layout warnings, so maybe this is helping—though it didn’t save me from hours of debugging.

More Auto Layout trouble

Funnily enough, I ran into an issue I’d already had when working on my custom mood card view. I spent ages switching constraints around and wondering why none were being applied to the bottom of the card view, thus giving it a height of zero. Reveal came in super handy yet again, showing me the height of zero, as well as a list of constraints where I could tell some were conspicuously missing.

It turned out that a whole chunk of my code wasn’t running at all. I’m using a loop to run through all the data I get back from the API, and I had if statements that checked if I was on the first or last loop through, so I could apply particular constraints. I wait until the last time through, for instance, to set constraints on the height of the card view itself, so that all its subviews had been created before I did so.

When I finally realised where the error was coming from, I remembered I’d blogged about it in part 2 of this series. The post kind of came in handy, as it confirmed my suspicions of where I’d screwed up, but the actual problem was slightly different, so I still had to figure out the solution on my own.

It turned out the bigger issue was the way I was using the for loop to run through all the data in the API response. I had based the code on the way I’d done previous group cards, like the mood card, where I use custom classes to create and lay out views for the data in each attribute. Since I’m doing everything for the weather card inside the weather group class, rather than handing each attribute off to its own class to be rendered, all I needed to do in the for loop was put each attribute’s value and name into a dictionary. Then I could do all the layout code outside the loop, using the dictionary to grab the data I needed. No need for checking if I’m on the first or last time through the loop anymore.

Josh helped me figure this out, and now my code is much cleaner. Too bad I’d spent all that time debugging already.

Getting the icon just right

The other main issue I had was getting the weather icon image to show up how I wanted. The original image is 480 x 400px, so it’s not quite square, but I wanted it to fit into a square view on the right-hand side of the card.

Part of the issue was that I didn’t want to constrain it at the bottom, because I was constraining the bottom of the card view to either the bottom of the weather icon, or the bottom of the weather summary text (e.g. “Partly cloudy throughout the day”), depending on which one ended at the lowest point.

[_view addConstraint:[NSLayoutConstraint constraintWithItem:_view  
attribute:NSLayoutAttributeBottom  
relatedBy:NSLayoutRelationGreaterThanOrEqual  
toItem:_weatherSummaryLabel                                                              attribute:NSLayoutAttributeBottom                                                             multiplier:1.0  
constant:8]];


[_view addConstraint:[NSLayoutConstraint constraintWithItem:_view                                      attribute:NSLayoutAttributeBottom                                                              relatedBy:NSLayoutRelationGreaterThanOrEqual                                toItem:_weatherIcon                  attribute:NSLayoutAttributeBottom                                          multiplier:1.0  
constant:8]];

(Martin Kenny helpfully pointed out this solution on Twitter after I shared an if else I was using in my post about the mood card. Definitely like this approach better—thanks Martin!)

Constraining the icon’s image view to the RHS but not at the bottom meant the image view stretched to its full 400 px tall, even though it wasn’t nearly that wide.

Again, Reveal was helpful here to see what size and shape the UIImageView was taking up, since I was using UIViewContentModeScaleAspectFit, which made the icon itself appear the size and shape I wanted it to, inside the super tall view.

My first attempt at fixing the icon problem was to figure out the ratio of the original image’s height to width, which was 0.8:1. I added a constraint that made the image view’s height equal to its width, with a 0.8x multiplier. I felt pretty cool just for thinking of that… until it didn’t work.

This did make the icon look better, but it stopped respecting its width constraints and grew so it was compressing the summary text label to have a width of zero. When I figured this out, I added compression resistance adjustments to both views, so the icon couldn’t squash the summary label, and would have to stay in its small width to the right of the card.

[_weatherIcon setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];  
[_weatherSummaryLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];

I was feeling pretty proud of myself at this point, since I usually manage to avoid touching priorities like compression resistance in my layouts. This time I not only had to give in and try them out, I managed to use them effectively as a means to achieve the end I wanted. Which is what programming is all about, right?

Well, I did have one more hiccup. I tested the layout on different phone sizes and realised it wasn’t consistent across small and big screens. I wanted the icon to take up roughly the same amount of space on the card regardless of the phone size. So I added a constraint that stopped the icon’s image view from ever growing larger than .25 * the width of its superview.

This didn’t work because of the compression resistance priorities I’d already set, and the fact that I had a constraint to make sure there was always some space between the summary text label’s trailing edge and the leading edge of the icon. With my new constraint, the icon kept getting smaller to accomodate the gap it needed at its leading edge and the high priority of compression resistance on the summary text label.

The answer to this wasn’t too hard, though. With the new constraint on the icon to keep it at .25 * the card’s width or smaller, I could switch the priorities of compression resistance so the summary text label has a low priority and the icon has a high one. At its max width, the icon still leaves plenty of room for the summary label, and since the label can wrap and the card’s bottom grows to accomodate the bottom-most edge of either the label or the icon, everything fits nicely on all screen sizes.


Phew! That’s a lot of constraint debugging.

I’m super close to beta testing the dashboard now. I have one major bug to look into, and some admin setup to get it ready for a new beta version. Once it’s in the hands of testers I’ll focus on bug fixes and adding insights—the last missing piece of the dashboard!

I won’t tell you what’s planned for my first big post-dashboard focus, but it’s pretty exciting. I’m looking forward to writing all about it.


If you’re an Exist user, have a play with the API. And let me know if you’re not beta testing the iOS app already, and I can add you to the list.