the Atomic Birdhouse Feed http://atomicbird.com/blog Kirby Thu, 10 Nov 2016 22:26:20 +0000 The blog feed for atomicbird.com Migrating from Dropbox http://atomicbird.com/blog/migrating-from-dropbox http://atomicbird.com/blog/migrating-from-dropbox Thu, 10 Nov 2016 00:00:00 +0000 Over the past year or so I've been trying out Resilio Sync (formerly BitTorrent Sync) as a possible alternative to Dropbox. It's gradually improved to where I think I can rely on it. With my Dropbox subscription up for renewal in a couple of weeks, now's the time. In this post I'll describe how to set up Resilio to get a Dropbox-like experience.

What I want from file syncing

I've used Dropbox for several years now. Their original feature is the only one I've ever cared about. I get a folder on my Mac that transparently syncs to my other Mac. I can also access files from an iOS app. Dropbox has built a bunch of other stuff onto their service, but I don't care about any of that. I want files to sync between Macs, and that's it.

For a variety of reasons I've been moving toward having direct control over my online data instead of using external services. For example, rather than rely on Cloud App to quickly share files online, these days I prefer Dropshare with my own server. It does more or less the same thing but I control where my data goes. It won't disappear because some company has been aqui-hired by another one or whatever. And so it is with Dropbox. It's not exactly the same situation since files on Dropbox still exist on my Mac. But I'd prefer to keep things more directly under my control if possible.

I looked at a variety of sync options and rejected most of them. Reasons varied, but I found that the quickest test of a sync system was checking how it handled symbolic links. My only real technical gripe with Dropbox is that it won't copy symbolic links as symbolic links. Instead it follows the links, which can cause duplicate files.

Some would (and have) argued against this as the right way to do things. No matter; it's the right way for me. Most sync services fail this in one way or another. Some resolve the links, Dropbox-style. Some ignore them. Often the services would have other faults, but this test fails so frequently that it's the first thing I try when evaluating a sync system. Resilio handles symbolic links the way that I consider to be correct.

Resilio also offers a couple of nice benefits. It'll sync directly from one Mac to another over my local network without needing to upload everything to a server first. And I can set up as many shared folders as I want, in any location on my Macs.

I should clarify that Resilio is sync software based on the BitTorrent protocol. They make an app that runs on your Mac (or other computer) but they don't provide an online sync service. You can add one of your own if you like. More on that later.

Syncing from one Mac to another

In the simple case of syncing between Macs it's, well, simple. There are several steps, but only because I'm trying to be as detailed as possible.

  1. Download the app on both Macs.
  2. On one Mac, add a folder to Resilio. addfolder Either the "standard" or "encrypted" folder options work here. I suggest "encrypted", for reasons I'll get into later. It allows keeping files encrypted but doesn't require encryption. Choose any folder on your Mac that you want to sync. As long as you can read/write the folder from your account on the Mac, it's fair game.
  3. Copy one of Resilio's unique keys for this folder. You'll find these by clicking the "options" button for the share, and then "Preferences" in the menu that pops up. sync-options. Then click "View Key" in the window that appears. There are three types of key; you'll want "Read & Write" to sync between Macs. There's also an "encrypted" option I'll cover later, and a "read only" key.
  4. Get this key to another Mac however you like. Keep it secret.
  5. On this other Mac, add a folder, but this time select "Enter a key or link". Enter the key.
  6. Resilo will ask you which folder to use on this Mac. Choose or create one. It doesn't have to have the same name or be in the same location as on the first Mac.

Now your Macs will sync files in these folders, directly. Add multiple Macs if you have more.

For a lot of people this is all you need. You might be done! The only real drawback here is that syncing will only happen between Macs that are online at the same time. If there are several Macs sharing a folder, they don't all have to be on at once. If one of the Macs is always online, it'll sync with any others when they're online, and act as the central sync for all of them.

But server space can be rented so cheaply these days that you don't need to keep one Mac on all the time.

Adding a virtual private server to the mix

I've been talking about adding Macs to Resilio, but it also runs on other platforms. Most interesting for this post, it runs on Linux and can be configured remotely. That means you can buy space at any VPS provider and add your virtual server(s) to the mix. That gives you an always-on computer to sync with for just a few bucks a month.

I run Resilio at Digital Ocean. They've worked well for me in the past, but if you have a VPS service you like, the'll probably work just as well. If you don't and you use that link to Digital Ocean, we'll both get a referral bonus. Depending on your setup it could be worth 2 months of service.

Your VPS doesn't need a lot of CPU horsepower or memory. It needs enough disk space to hold your synced files. I'm currently using Digital Ocean's $5/month plan for Resilio, which gets me 512MB of memory and 20GB of storage. If you use them, their new "volumes" option is a good way to boost disk space without buying more memory that you don't need.

So let's run through the setup. I'll note that while this is not extremely difficult, it's nowhere near as easy as Mac-to-Mac syncing, and requires comfort with Linux command lines. I'd like to suggest a simpler approach but I don't know one right now.

  1. Create a new VPS-- a "Droplet" at Digital Ocean. Mine is a basic Ubuntu Linux 16.04 setup. Digital Ocean offers to install an SSH key when creating the droplet. Do this, because command line access will be useful.

  2. Create a user account on the VPS. This isn't strictly necessary but I don't like to log in as root unless I have to. I'll set up an account that can use sudo and then use this account when I log in. SSH in to your VPS (as root, initially), create the user account, and give it sudo powers:

    % adduser <username>
    % usermod -a -G admin <username>
    
  3. Install Resilio. I followed the steps described at Resilio's site, somewhat simplified here to fit my needs. Since I'm using Ubuntu Linux, these steps use the Debian package manager. If your Linux prefers RPM, see Resilio's article for details.

    • Add Resilio package information to apt-get:

      % sudo cat > /etc/apt/sources.list.d/resilio-sync.list
      deb http://linux-packages.resilio.com/resilio-sync/deb resilio-sync non-free
      
    • Add the public key so apt-get can verify Resilio packages:

      % wget -qO - https://linux-packages.resilio.com/resilio-sync/key.asc | sudo apt-key add -
      
    • Tell apt-get to update, and install Resilio. This step also creates a new user account called rslsync, which Resilio will use when running.

      % sudo apt-get update
      % sudo apt-get install resilio-sync
      
    • Add Resilo as a system service:

      % sudo systemctl enable resilio-sync
      
  4. Now Resilio is installed and running but not yet configured. It has a web based interface, but it only listens on 127.0.0.1 so you can't get to it remotely-- unless you use SSH port forwarding. I added something like this to my ~/.ssh/config.

    Host [my VPS's IP address]
    User [my username created above]
    LocalForward 9888 127.0.0.1:8888
    IdentityFile ~/.ssh/id_ed25519
    

    With this in place, and an SSH session active, I can point my browser to http://localhost:9888/gui. SSH turns that into a connection to port 8888 on the VPS, coming from 127.0.0.1.

  5. Before configuring Resilio, you need to give it some place to store its files on the VPS. This can be anywhere you like, so long as the user rslsync created above has write access to it. I decided to use /opt/resilio:

    sudo mkdir /opt/resilio
    sudo chown rslsync /opt/resilio
    
  6. Now, finally you can configure Resilio using the web interface. The web interface is nearly the same as in their Mac app. To add an existing shared folder, copy the folder key like you did earlier, only this time copy the "encrypted" key instead of the "read & write" key. This way, your files will be encrypted on the VPS. If the VPS is insecure for any reason, your files won't be readable there.

    When Resilio asks for a directory to keep the files in, navigate to the location you chose in the previous step and use the GUI to create a new sub-folder. If your share is named "mystuff", create /opt/resilio/mystuff. Using a sub-folder will make it easier to add other shared folders in the future, which will all go in the same place.

And you're done! You have always-on automatic file syncing. Just add files to whichever folders you added to Resilio. Start moving files from your Dropbox folder into your new Resilio folder(s).

Soon after reaching this point I got an email from Dropbox that began "We noticed that you recently deleted a large number of files from your Dropbox." Well, yeah.

A couple of other notes

  • Resilio has an iOS app, which can browse and download documents. You add folders as on Mac or Linux. Unlike on a Mac, it doesn't attempt to sync all files. Instead you browse the shared folder and can download individual files.

  • One VPS might not be enough redundancy for some people. That's fine; you can set up as many as you need. Run them at different data centers, or at different VPS services. Think of it as applying RAID 1 at the VPS level.

Keeping Dropbox, a little

I can't completely drop Dropbox. Mainly because their API means that various apps can sync files via Dropbox. I really like Editorial on iOS, for example, and it uses Dropbox to sync documents across my devices. Reslio has an API but I haven't looked into it yet. In any case it's nowhere near as widespread as Dropbox.

The majority of the files I sync aren't managed directly via the Dropbox API, though. They're just files. I can keep Dropbox's free account level and keep using my apps, while keeping the bulk of my file syncing in Resilio.

]]>
Date Math is Hard, Let's Do it Tomorrow http://atomicbird.com/blog/date-math-is-hard-lets-do-it-tomorrow http://atomicbird.com/blog/date-math-is-hard-lets-do-it-tomorrow Sun, 06 Nov 2016 00:00:00 +0000 This post is based on a lightning talk I gave at CocoaConf San Jose a couple of days ago.

It's that time of year-- the time when summer time, or "Daylight Saving Time" as we for some reason call it in the USA-- is ending. That time when a developer's thoughts turn to date math and what a pain in the ass it can be.

Why is this so hard to get right? It'd be nice to think that dates and times follow predictable routines whose cycles all use nice round (or at least consistent) numbers. However:

IMG_7180

  1. The universe doesn't care about regular numbers. The length of a year doesn't divide into an integer number of days. Convenient time periods like months are no better. The Earth's rotation isn't entirely consistent. So we have leap years, and leap seconds. Months have variable lengths.

  2. Living on a planet which rotates on an axis that doesn't point toward the sun means that time zones-- or something like them-- are inevitable. There have been various ideas about using a single global time zone, but that just ends up reinventing zones in a new guise (instead of tracking what time it is in different places, you end up needing to know what time the day starts in different places). Sadly the rules of time zones are in the hands of literally every government in the world. They can change at any time for reasons that don't have to make sense.

  3. Times and dates are extremely familiar concepts. Anyone who uses a calendar and/or a clock of some sort tends to see them as mundane, routine things. That leads to complacency when writing code, and bugs inevitably result. Developers take date math for granted because it seems simple. Next thing you know you're looking at an app that shows two Sundays in a week and nobody's quite sure why.

Doing all of this correctly often means using the iOS frameworks instead of mistakenly thinking you know what you're doing. That hands the problem to people who do this kind of thing all the time and who are a lot less likely than you to screw it up.

[NS]Calendar is your friend and will gladly help you out here. In iOS 8 it gained a bunch of useful new methods, too, so I recommend looking closely at the docs. But it's not always so simple as finding the right framework method. In the rest of this post I'll go through some common problems and how to avoid them.

1di9h0

When is tomorrow?

This is such an obvious question that it's a pity so many developers get it wrong. The question is usually phrased as something like, how do I get a date that's the same time tomorrow as it is right now?

// WRONG
let now = Date()
let tomorrow = now.addingTimeInterval(24*60*60)

Get the time 24 hours from now! Or maybe 23. Or 25. Or maybe 24 hours and one second. This is a fun one because it will very often give the correct answer. If you used this code most days, it'd be correct. But if you had used it on Saturday in the USA it would be off by an hour-- because daylight saving time ended that night. This code is bad unless you actually want exactly 24 hours without considering time changes.

In this case [NS]Calendar has you covered, with this handy method for doing date math:

let tomorrow = Calendar.current.date(byAdding:.day,
    value:1, to:now)

What is today?

It sounds like a trivial concept. Maybe your app needs to find events that happen "today". And you can get that by looking at the time from midnight to midnight. That's today, right?

Right?

// Wrongy McWrongface
let startOfDay = Calendar.current.date(bySettingHour:0,
    minute:0, second:0, of:now)

let startOfTomorrow = startOfDay.addingTimeInterval(24*60*60)

The first line takes today and gets a new date with the hour, minute, and second set to 0 to get the most recent midnight. The next line adds 24 hours, because we're already being sloppy so why not?

This one's even better than the last. In the USA we're used to the idea that summer time starts at 2 AM. But in some time zones it starts at midnight. One second it's 11:59:59 and the next it's 01:00:00 with no midnight. The code is fine, usually, but will have weird bugs that come up only occasionally and only in some countries. Oops.

The code above uses [NS]Calendar but still makes bogus assumptions. A better approach would be:

let startOfDay = Calendar.current.startOfDay(for:now)

let startOfTomorrow = Calendar.current.date(byAdding:.day,
    value:1, to:startOfDay)

If you're looking for when "today" started, ask for when today started. Don't assume you know. While you're at it, don't forget that "today" is a concept that depends on the local time zone. Today for you is different than today for someone a couple thousand miles east or west of you.

Artisanal Locally-Grown Time

how-do-they-work

So how do you deal with time zones, anyway?

First, of course, don't ever save local time. Save times and dates as UTC. Convert to and from local time when needed. When presenting dates to a user, use [NS]DateFormatter to convert the UTC date to an appropriate time zone. If your app lets the user enter times and/or dates, convert that to UTC before storing it. Whatever you do, do not attempt to do your own time zone conversions.

True story: I once worked on an app where all time zone support was handled with the following values, which were hard coded in the app:

Time Zone UTC offset
Eastern -18000s
Central -21600s
Mountain -25200s
Pacific -28800s

Those are the four best-known time zones in North America with the GMT offsets for winter. Oh geez, I thought. It's bad enough that this doesn't account for moving clocks forward and back like we do. But it also misses the fact that Arizona doesn't mess with daylight "saving" time. It's correct maybe 45% of the time, if I'm generous and assume that the app won't ever be used outside of the continental USA.

True fact: there are several hundred time zones in the world. A time zone combines an offset from UTC with rules about how that offset changes based on the date. Most time zones have summer time, but not all. Those that do have it usually disagree about when it starts, or ends, or both. Any difference means you have a different time zone.

Fortunately iOS encapsulates all this in [NS]TimeZone. It uses the IANA Time Zone Database which is a de facto standard used by pretty much every computer company everywhere.

If you need to calculate dates in a different time zone, use [NS]Calendar and tell it what time zone to use. So if you need to know when "today" is somewhere else, just choose the right time zone.

var myCalendar = Calendar.current
if let timeZone = TimeZone(identifier: "America/New_York") {
    myCalendar.timeZone = timeZone
}
let startOfDay = myCalendar.startOfDay(for: now)

How do I convert a date to a different time zone?

You don't, if by "date" you mean [NS]Date. It represents a single instant in time, everywhere (ignoring relativistic effects). That means if you use the following and someone on a different continent does so at the same moment, you both get the same result!

let now = Date()

Don't forget: a Date has no time zone information. Also, don't forget: A Date has no time zone information. I realize that technically those are the same thing, but it's such a common mistake I thought it was worth mentioning twice.

Since there's no time zone information, converting to a different time zone doesn't make sense. If you think you need to convert a date to a different time zone, you're probably already in deep trouble with dates. It's time to stop what you're doing and reconsider your life choices.

How long is a month?

This one's more obvious than the others because of course you know that different months have different lengths. But remember, it's all edge cases. What if you want to get the date one month from today? What if "today" happens to be January 31? February 31 maybe?

If you're asking that question on the last day of the month, you probably want the last day of next month. You can use that handy date(byAdding:, value:, to:) method from earlier here, but you have to be careful how you do it.

One way is a cumulative calculation, adding one month to the start date, and then one month to that, etc:

// Get a date on 31 January 2017
let startDate = (Calendar.current as NSCalendar).date(era: 1, year: 2017, month: 1, day: 31, hour: 12, minute: 0, second: 0, nanosecond: 0)
print(startDate)

var currentDate : Date = startDate!
for i in 1...12 {
    currentDate = Calendar.current.date(byAdding: .month, value: 1, to: currentDate)!
    print(currentDate)
}

Things start out OK, but quickly go awry:

2017-01-31 19:00:00 +0000
2017-02-28 19:00:00 +0000
2017-03-28 18:00:00 +0000
2017-04-28 18:00:00 +0000
2017-05-28 18:00:00 +0000
2017-06-28 18:00:00 +0000
2017-07-28 18:00:00 +0000
2017-08-28 18:00:00 +0000
2017-09-28 18:00:00 +0000
2017-10-28 18:00:00 +0000
2017-11-28 19:00:00 +0000
2017-12-28 19:00:00 +0000

January 31, then February 28? Good. But one month after that is March 28. You get stuck on the 28th instead of the end of the month.

Another approach is to keep adding months to the start date, discarding intermediate values when you don't need them anymore:

for i in 1...12 {
    let nextDate = Calendar.current.date(byAdding: .month, value: i, to: startDate!)
    print(nextDate!)
}

This looks better:

2017-01-31 19:00:00 +0000
2017-02-28 19:00:00 +0000
2017-03-31 18:00:00 +0000
2017-04-30 18:00:00 +0000
2017-05-31 18:00:00 +0000
2017-06-30 18:00:00 +0000
2017-07-31 18:00:00 +0000
2017-08-31 18:00:00 +0000
2017-09-30 18:00:00 +0000
2017-10-31 18:00:00 +0000
2017-11-30 19:00:00 +0000
2017-12-31 19:00:00 +0000

There we go, last day of the month every time. Get up and do a happy dance, you've earned it.

What can I do?

Entering Mountain

When it comes to dates, everything you know is wrong. At least sometimes. Code that looks like it works may well have weird bugs that only manifest on specific dates or in specific locations.

So in short: assume nothing. Even the most obvious things, like when today started, are likely wrong at some point. Use the iOS frameworks whenever possible.

But even then, test your results! "I used a framework method" doesn't mean you actually did the right thing.

Good luck, we'll all need it.

]]>
My other blog http://atomicbird.com/blog/my-other-blog http://atomicbird.com/blog/my-other-blog Fri, 09 Sep 2016 00:00:00 +0000 I try to keep this blog on topic, sticking to technical posts of interest to iOS and macOS developers. So when I wanted to write about something else I set up a different blog unrelated to my business.

As some of you are aware, when I'm not working on apps I'm also a radio DJ, at KRCC in Colorado Springs, CO. If you aren't, you might still have noticed how I used to win prizes at "Stump the Experts" at Apple WWDC by identifying song. While DJing I get to listen to a ton of great music, and I wanted to write about what I'm listening to. So I'm introducing Tom Swift FM (my on-air nickname is "Tom Swift", which seemed apt for a Swift developer). I'll post about whatever great music I've been listening to lately.

I hope you'll check it out. That's all I'll say about it here.

]]>
Stump 360 III: The Search for Stump http://atomicbird.com/blog/stump-360-iii http://atomicbird.com/blog/stump-360-iii Fri, 12 Aug 2016 00:00:00 +0000 Later this month I'm hosting Stump 360 III: The Search for Stump, the third annual Stump 360. It's part of 360iDev in Denver. It's on August 23, 4:45pm - 6:00pm.

In case you're not familiar with Stump 360, here's some possibly interesting information.

Stump is sort of approximately a game with two teams, the audience and the panel. It's a quiz/trivia style event where each team poses questions to the other in the hope of stumping them.

Stump 360 Jr: Not Dead Yet

But don't take the game too seriously. I certainly don't. Ideally everyone will have some fun, and just maybe, some people will get correct answers to some of the questions. Having fun is more important than being able to recite complex technical answers from memory.

The panel is made up of volunteers from the speakers at 360iDev. The audience is, well, anyone else who shows up.

When I ask people if they'll be on the panel, it's common for them to respond that they're not sure if they really belong in such an esteemed group. Leaving the psychology of impostor syndrome aside, it's no accident that the word "expert" does not appear in the event title. Anyone who can convince John Wilker that they should speak at 360iDev is more than qualified, as far as I'm concerned.

Questions can be pretty much anything related to iOS and/or Apple in general, including historical Apple questions. Also, anything covered in any 360iDev talk is fair game as a question. When you attend a 360iDev talk, know that there's a quiz on it later, and that quiz is at Stump 360.

How it works

Stump 360 Jr: Not Dead Yet

  • I'm the host, so I'm not on either team. I'll take care of the questions and whatever else needs to happen to keep things running.
  • I'll be assisted by the lovely and talented Joe Pezzillo, in the role of the "immoderator".
  • Questions for the audience will be displayed on slides. Audence members volunteer to answer questions.
  • Audience members can submit questions for the panel. In the past these have been written on 3x5 cards.
  • Answers don't have to be given immediately-- people can go off and work on a question for a while if they like.
  • The host (that's me) distributes questions from the audience to panelists.
  • Score is based on the correctness of answers. Partial credit may be awarded. Bribing the host is permitted.

Teamwork is allowed! Nobody has to answer the question on their own unless they want to.

Also, this is an open book test, so anyone answering a question is free to use whatever resources they have at their disposal to find the answer.

Where it came from

Stump 360 is, to borrow a phrase from Eric Idle, "lovingly ripped off" from the Stump the Experts session that was once a staple of Apple's Worldwide Developer Conference. It was always a highlight for me. When WWDC reached the point that it was hard to get tickets, I thought it would be nice to have a similar event for people who couldn't get in. And so Stump 360 was born. It's obviously not quite the same thing (we don't have experts with 30 years of experience at Apple, for example) but we do what we can to carry on the tradition.

Got a question for Stump?

I need your questions! If you know something interesting about iOS and/or Apple that would make for a good question, send it in to me! Good questions include anything related to iOS and/or Apple, or anything covered at 360iDev, though ideally not something that can be Googled in under 30 seconds. If it's a trick question, all the better.

I uploaded the complete list of questions from 2015 to help get you thinking about good questions for Stump. I think my favorite was one from Ben Lachman, who asked:

What Foundation class contains two public methods that provide the same functionality excepting that one is spelled incorrectly?

See you there!

It's almost time. If you're coming to 360iDev, I hope to see you at Stump! If you're not coming to 360iDev, there's still time to register!

Pictures of Stump 360 2015 in this post are by Fuad Kamal and are used by permission.

]]>
Using Stack Views in Table Cells http://atomicbird.com/blog/uistackview-table-cells http://atomicbird.com/blog/uistackview-table-cells Thu, 04 Aug 2016 00:00:00 +0000 One of the cool things UIStackView can do for you is make it easy to dynamically update your app's user interface while it's running, with smooth animations and not a lot of code. My recent talk at iOSDevCamp DC covered some techniques. Natasha the Robot wrote a couple of great posts based on my talk, and today I'm going to talk about another unexpected (to me?) use of stack views.

Animated Updates with Stack Views

Stack views exist to figure out the layout constraints for their arranged subviews. But only for the stack views that are visible. It might seem obvious but stack view layouts don't consider subviews that don't appear on the screen.

The great thing about this is that you can dynamically update your UI just by changing the value of the isHidden property on an arranged subview. Change the value and the stack view recalculates the layout for its new collection of visible subviews and updates the UI as needed. These updates happen instantly but they can be wrapped in a UIView animation block to smooth them out.

If you hide a view like this it conveniently disappears from the layout, but it's still a subview of the stack view. So you don't have to do anything special if you want to bring it back later on. It disappears but doesn't get removed from the view hierarchy or deallocated.

So yeah, if you have a button in a stack view you can make it disappear and update a whole mess of layout constraints to match the new button-free layout just by doing this:

button.isHidden = !button.isHidden

And if you want to make it nice and smooth and animated it's not much harder:

UIView.animate(withDuration: 0.3) {
    button.isHidden = !button.isHidden
}

That syntax seems a little weird to me since animations usually imply gradually changing the value of something. Here the value is a Boolean, though. It's not that the latest version of Swift added intermediate Boolean values, this just tells the layout system to animate the updates on the screen.

Designing Expandable Table Cells

So let's apply these automatic layout updates to a common design pattern in iOS apps: the table cell that when tapped expands to show more content for the selected table entry. As demo I wrote an app that gets the top 25 tracks from the iTunes store and shows them in a table. If you tap on one of the cells it expands to show audio playback controls that could be used to play a preview of the track. (I didn't actually implement audio playback because I wrote this to demonstrate the dynamic UI).

ExpandingCells-4

Incidentally, although this is a common design pattern, the actual UI above is an example of why engineers generally should not be allowed to do UI design.

The table cells are designed with a stack view as the only subview of the cell's content view:

Main.storyboard

The stack view has two arranged subviews. The top subview shows track information and is always visible while the bottom audio playback view is only visible when the cell is expanded. Both of these are just containers, with the image, labels, buttons, etc laid out using traditional autolayout constraints.

There are a couple of things I should clarify about how this works:

  • The stack view distribution is "fill", so it needs to know how tall its arranged subviews are to determine its own height. Since this stack view's arranged views don't provide an intrinsic content size (unlike labels or buttons, which do) both of them have constant height constraints.

  • I set the table view's cell height to be large enough to hold everything contained in the stack view. This just makes Xcode leave enough space for me to add everything that could possibly show. It won't affect the run-time sizing, for reasons discussed a little later on.

cell height

Making it Happen

The first step is about getting the table view to notice that cells can expand and collapse. Expanded cells will be taller than collapsed cells, so the table needs to be ready to get the cell height from the cell. So turn on self-sizing cells for the table view.

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 85

The estimatedRowHeight value is the height of the top part of the expandable cell, i.e. the part that's always visible. This is why setting the cell height above won't affect the app when it runs-- because I'm telling the table view that it should ignore that size and ask each cell how tall it wants to be.

Since I want to make the audio playback view appear and disappear, the table view cell class has an outlet pointing to it called audioPlaybackView. Following the discussion above about how stack views only arrange their visible subviews, the expand/collapse behavior is simply a matter of showing or hiding audioPlaybackView and then getting the table view to notice the change.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    if let cell = tableView.cellForRow(at: indexPath) as? StoreTrackTableViewCell {
        cell.audioPlaybackView.isHidden = !cell.audioPlaybackView.isHidden
        tableView.beginUpdates()
        tableView.endUpdates()
        tableView.deselectRow(at: indexPath, animated: true)
    }
}

The beginUpdates/endUpdates shuffle there is included to make the table view notice that the cell height has changed.

Finally, I want the cells to start off in their collapsed state. When the table view loads, no cells should be expanded. That's easy to do with a property observer on the IBOutlet for the audio playback view.

@IBOutlet weak var audioPlaybackView: UIView! {
    didSet {
        audioPlaybackView.isHidden = true
    }
}

One other detail you'll probably want is to track which cells are currently expanded, so that table cell reuse doesn't unexpectedly expand multiple rows during scrolling. I won't cover that here but it's in the sample project.

Yay it works! But wait...

At this point my project worked, but I was getting a bunch of the dreaded Unable to simultaneously satisfy constraints messages when I ran the app.

2016-08-04 14:31:42.343368 Top25[23475:3079180] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x600000289ab0 UIView:0x7fed2360c860.height == + 51   (active)>",
    "<NSLayoutConstraint:0x608000285e60 'UISV-hiding' UIView:0x7fed2360c860.height ==   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000289ab0 UIView:0x7fed2360c860.height == + 51   (active)>

The first constraint was immediately familiar-- it's the fixed height constraint of 51 on the audioPlaybackView for each cell. The second one I didn't recognize, but its name gave me a clue. UISV-hiding, you say? And it conflicts with the fixed height constraint on a view that I'm hiding. And I'm getting one of these errors per visible cell. Hmmm....

These messages give some insight into how stack views manage hidden arranged subviews. It's apparent that one of the stack view's changes is to apply an extra height constraint (presumably of height 0) to make the view disappear from its layout. One that in this case conflicts with the fixed height constraint.

Knowing that, the fix is straightforward. When the audio playback view is hidden I don't care what its height is. It's not visible so who cares. So I'll turn down the priority on the height constraint just a bit so that the stack view's hiding constraint will take precedence.

constraint priority

And now, all is well. The cells expand and collapse when tapped and the layout engine doesn't have any reason to complain about it.

Other possibilities (a.k.a. exercises for the reader)

This example implements a simple expandable table cell, but it could be extedned in some interesting ways.

  • The stack view might contain three or more subviews, which could be switched in and out as needed. Maybe the audio playback controls would be visible when the cell loaded, but could be replaced by a different subview in some situations.

  • The contents of each cell section could be redone to use stack views as well, making it easy to update their contents on the fly. For example the "play" button in the audio playback view might be replaced with a UIActivityIndicatorView while the audio preview is loading. It could then be replaced with the "play" button when loading finished.

Demo Project

The code used for this post can be found at GitHub.

]]>
Flexible UI Design with UIStackView http://atomicbird.com/blog/uistackview-flexible-design http://atomicbird.com/blog/uistackview-flexible-design Thu, 28 Jul 2016 00:00:00 +0000 As I mentioned in my last post, last week I did a talk at iOSDevCamp DC where I talked about UIStackView, a relatively new UIKit class that's my new favorite thing in iOS development. I'm going to cover some of the more useful things UIStackView can do in posts here, which will fall more or less into two categories:

  1. Simplified and flexible UI design, making common UI patterns easier to implement and modify.
  2. Dynamic UI updates when apps are running, including UI animations and updates triggered by size class changes.

Today I'll hit point #1 above. The cool, useful thing is this:

Stack views make (most of) your autolayout constraints unnecessary,

for common linear UI layouts.

Side by Side Comparison

So let's take a look at what UIStackView can do at the design stage. I implemented a simple demo app that displays a map view in the top part of the screen with location-related details below.

MapDemo Initial

In order to make it interesting and provide a literal side-by-side comparison, I used a tab view controller and implemented the same UI in both tabs. In the left tab I added autolayout constraints to put everything in its place and make sure it stays there. The tab on the right uses nested UIStackView containers, and as a result needs far fewer explicit layout constraints. Other than that the tabs look the same, and even use the same view controller class.

If you're new to UIStackView, keep in mind that both versions use autolayout to position the UI elements, and get all of the advantages that come with that. Both versions respond well to device rotation, both work well with different screen sizes, etc. The difference is that in one case I created the constraints, while in the other I configured a UIStackView and let it work out what constraints were needed.

So how does that work out?

Here's what the constraints and view hierarchy look like when not using stack views.

MapDemo constraints non-stack 1

It's not extremely complicated. Each view has constraints that specify both position and size. The layout works just as well for different screen sizes or when the device rotates. The only detail that required any thought was working out how to constrain the text fields to be almost but not quite half as wide as their containing view.

Here's the same thing with the alternate implementation, which uses nested stack views for layout:

MapDemo constraints stack 1

There's a vertical stack view that fills the screen. Within that there's a horizontal stack view to hold the two side-by-side columns of UI elements at the bottom of the screen. And within those, there's another level of vertical stack views containing the vertically arranged text fields and labels.

None of the UI elements in the bottom part of the screen have any explicit constraints. They're still constrained, of course, but the stack view adds the constraints automatically. Everything works exactly as in the previous version. Of the constraints that do exist, half of them are just to make sure that the top level stack view fills the screen.

There's nothing wrong with adding your own constraints, and in some cases it's still necessary. But I've always followed the rule that the best code is the code you didn't have to write, and the same thing applies to layout constraints. If I can get results that are just as good with far fewer constraints, I know which strategy I'll use.

Of course the view hierarchy gets more complex in the stack view version. Instead of one containing view and a handful of subviews, it has several nested stack view levels. I maintain that this is strongly outweighed by the advantages. The extra nesting isn't as complex as it might seem at first, because it reflects the UI organization. The stack views reflect the logical, conceptual grouping of the views. They therefore reflect the mental model that the UI implies.

If the above wasn't enough to convince you of this, read on.

When Requirements Attack. Change! When Requirements Change!

The UI above shows four details of the user's current location, the latitude/longitude coordinate, the altitude, and the horizontal accuracy. Core Location also tells us the user's current speed and direction. What if we want to add those? And what if we want to put the new information in the middle of the existing fields?

MapDemo speed direction UI

Using traditional artisanally hand-crafted layout constraints, the process is something like:

  1. Move several existing views
  2. Add the new views
  3. Remove or adjust a bunch of constraints
  4. Add a bunch of new constraints

Let's see what that looks like in this case.

Again, it's not difficult, but it's a lot of tedious and error prone steps. But compare that to doing the same thing with a UIStackView based layout. The process is:

  1. Add the new views

That was... a whole lot easier. For results that work just as well. That was worth a bit of extra complexity in the view hierarchy.

Because of a limitation in Xcode, there's one step in that video that might not be clear. The first thing I did was to change the alignment of a horizontal stack view from "fill" to "top". Why? Because I'm about to change the heights of the views it contains, and I can't change them at the same time. If I change one, then the other, the results work with "fill" but Xcode gets confused and misaligns UI elements that are in the right place when the app runs. Changing the alingment to "top" makes Xcode happy and doesn't affect the final result.

After making Xcode happy, though. Wow. Add the new views and... I'm done. UIStackView gives me the constraints I need, and I go on to something else.

Update: Where's the Code?

After getting requests on Twitter, I've uploaded the Xcode project used for this post to GitHub. The tag that corresponds to this post's demos is the "blog-1" tag.

What's Next

That's not enough? OK, how about if I told you that stack views can also make it easy to make your UI highly responsive to device rotation and to add useful and cool animations to your user interface? Natasha the Robot covered some of this in her posts based on my iOSDevCamp DC talk. I'll be covering more of it in upcoming posts.

]]>
UIStackView talk at iOSDevCamp DC http://atomicbird.com/blog/iosdevcampdc http://atomicbird.com/blog/iosdevcampdc Tue, 26 Jul 2016 00:00:00 +0000 Last week I did a talk at iOSDevCamp DC, an annual event hosted by Luis de la Rosa. I talked about UIStackView, under the admittedly grandiose title of "Mastering UIStackView". I've used stack views for a number of things recently, as I've come to realize they're a lot more useful than a lot of introductory material might suggest.

I'll be writing about some of this in the very near future, but in the meantime the excellent Natasha the Robot has already done two blog posts based on stuff that I covered:

Sometimes when I do a technical talk I wonder if people really took anything away from it. Other times, someone does two great blog posts based on the talk just a couple of days later. Go read Natasha's posts!

]]>
File Coordination Fixed! http://atomicbird.com/blog/file-coordination-fix http://atomicbird.com/blog/file-coordination-fix Mon, 18 May 2015 00:00:00 +0000 I wrote a post a few months ago about sharing data between iOS apps and app extensions in which I recommended using NSFileCoordinator and NSFilePresenter. But I had to update the post to remove that portion when some helpful people pointed me to Apple Tech Note 2408, which read in part:

When you create a shared container for use by an app extension and its containing app in iOS 8, you are obliged to write to that container in a coordinated manner to avoid data corruption. However, you must not use file coordination APIs directly for this.

That basically meant that it was specifically unsafe to use these classes for what would seem to be one of their primary use cases. That sucked.

However last week the tech note was updated, and the above section now reads:

When you create a shared container for use by an app extension and its containing app in iOS 8.0 or later, you are obliged to write to that container in a coordinated manner to avoid data corruption. However, you must not use file coordination APIs directly for this in iOS 8.1.x and earlier. [emphasis mine]

That's great! In iOS 8.2 or higher, the obvious approach should now be safe. I've updated the original post to restore (and somewhat expand) the original discussion of file coordination.

]]>
Swift Generators and Sequences http://atomicbird.com/blog/swift-generators-sequences http://atomicbird.com/blog/swift-generators-sequences Mon, 23 Mar 2015 00:00:00 +0000

A large part of the Swift Standard Library is concerned with Generators, Sequences and Collections and functions that operate on them, so it’s pretty important to have a good understanding of them.

I was going to write a blog post about how to create and use Swift generators and sequences. But while I was trying to understand them myself, I found this post over at iOSDeveloperZone.

Go read that, and pretend that I wrote something as helpful.

]]>
Using Core Data with Swift http://atomicbird.com/blog/swift-core-data http://atomicbird.com/blog/swift-core-data Mon, 16 Mar 2015 00:00:00 +0000 Over the past month or so I've been diving into Swift, after many years of working with Objective-C on Macs and iOS. It's been a change but, gradually, I'm learning the Swift way of doing things. On the way I've run into a few bumps in the road when dealing with Core Data, and I thought would be useful to share how I got past them.

Xcode Generated Subclasses Considered Harmful

This is the main impetus for this post. Most other stuff I would expect people to work around eventually, but this one is kind of big.

Xcode's generated NSManagedObject subclasses are limited but useful in their own way. If you don't need much, they'll do. Everyone else would use mogenerator. That's if you're using Objective-C, though. With Swift there's a decent chance that using Xcode's subclasses will actually crash your app.

Both Core Data and Swift include the concept of an optional value. But when Xcode generates subclasses, it doesn't consider whether Core Data thinks the value is optional when generating the Swift file. It generates non-optional properties every time. This conflict means that you end up a Swift non-optional property that could reasonably have a nil value. The compiler sees that the property is not optional, so it doesn't mind that you aren't checking this (in fact, Swift rules mean it's not possible to check, because the property is not optional). This is exactly the kind of problem Swift's compiler is supposed to catch for you, but here it can't.

For example, suppose you configure a Core Data attribute like this:

optional-attribute

Since summary is optional, it's allowed to have a nil value. But if you have Xcode generate a subclass for this entity, you get

@NSManaged var summary: String

...which is not optional, and which the Swift compiler will therefore assume to have non-nil values. But the @NSManaged prevents it from complaining that you aren't initializing the property.

If you write code that treats summary as non-optional, the compiler won't mind. But if you try to access summary when it's nil, your app will crash.

This is a trivial fix, just make the property optional. Or even better, an implicitly unwrapped optional:

@NSManaged var summary: String!

But people who use Xcode's subclass code could reasonably assume that doing so is at least safe, even if maybe it could do more than it does.

If you're using mogenerator, you're covered for Core Data optionals. It makes sure Core Data optionals become Swift optionals. I'd take it a step farther and make all properties optional in Swift even if they're required by Core Data. Core Data doesn't enforce the "required" rule until you save changes, so even non-optional attributes can legally be nil at almost any time.

rdar://20153926

What is this thing... called @NSManaged?

Generated subclass attributes have an @NSManaged decoration, but what does that mean? Apple says

Apply this attribute to a stored variable property of a class that inherits from NSManagedObject to indicate that the storage and implementation of the property are provided dynamically by Core Data at runtime based on the associated entity description.

For Objective-C developers that means it's taking the place of @dynamic. In both languages, Core Data will do its internal magic to ensure that Core Data style accessor methods exist at run time even though they're not defined in your code.

From a Swift perspective, @NSManaged also tells the compiler that it doesn't need to care whether the property has a value before initialization is complete, even if the property is not a Swift optional. It's like declaring that this is a computed property with accessors to be provided later. However as the discussion above illustrates, this can be a really bad idea for non-optional attributes.

Regardless though, you do need @NSManaged unless you're providing custom accessor methods.

Getters and Setters

But what about when you need a custom getter or setter method for a Core Data attribute? Swift has an elegant way of linking accessor methods directly to the corresponding property declaration. The aptly named get and set blocks are just what you need. Except... if you add these blocks, the compiler will warn you that 'NSManaged' [is] not allowed on computed properties. How now, since this is a Core Data attribute?

Although the documentation could be read as meaning that @NSManaged is required, it actually isn't. It's only needed if you'll be letting Core Data handle the accessors. If you're providing your own, drop it. Core Data's accessor magic is not documented but it seems you can't just override it like you'd override other methods.

A minor annoyance is that since you now have a computed read/write property, you can't provide custom setter behavior unless you also provide a custom getter. Even if you don't need to change the default getter behavior, you need to provide the code.

Put it all together and the declaration and custom accessors for a property named text might look like this:

public var text: String?
        {
        set {
            self.willChangeValueForKey("text")
            self.setPrimitiveValue(newValue, forKey: "text")
            self.didChangeValueForKey("text")
            
            self.dateModified = NSDate()
        }
        get {
            self.willAccessValueForKey("text")
            let text = self.primitiveValueForKey("text") as? String
            self.didAccessValueForKey("text")
            return text
        }
    }

[Thanks to Casey Fleser for pointing out that the initial version of the code above left out calls to willAccessValueForKey and didAccessValueForKey.]

An array of WAT?

When fetching data, the return type of executeFetchRequest:error: is [AnyObject]?. That makes some sense since the only guaranteed detail is that the result is either nil or an array of something. The actual array contents depend on how your fetch is configured. You'll make things easier on yourself if you downcast the result to something more specific. What you need depends on the result type you're asking for:

  • NSManagedObjectResultType (the default fetch type, a.k.a. "normal" fetching): if you have a custom class for your entity, you'll get an array of that, so downcast to [MyClassName]?. If you don't (or don't know for some reason) use [NSManagedObject]?.
  • NSManagedObjectIDResultType: Use [NSManagedObjectID]?.
  • NSDictionaryResultType: This returns an array of dictionaries, each of which has String keys and AnyObject values. Use [[String : AnyObject]]?. The value types match up with the Core Data attribute type. You'll need to know those types is to use the values, and downcast to String?, Int?, etc.
  • NSCountResultType: I've never seen this used, since countForFetchRequest:error: exists. But for the sake of completeness, use [Int]? here.

Let's Be Careful Out There

Like I said earlier, Xcode's generated code was the main reason for writing this. If you have Xcode generate your subclasses, look carefully at what it gives you. If you don't want to worry about that (and want better subclasses overall), use mogenerator.

]]>