Buddybuild is a CI, CD and user feedback platform built for mobile development teams - sign up for free and start building better apps!

Apple platforms make heavy use of "reverse-DNS" identifiers.

They appear everywhere from our Application's Identifier all the way down to the Dispatch Queues we create in our code.

These identifiers are somewhat opaque in a conceptual sense, and have been around in programming for many years.

We create or set them once, and sometimes reference them in our code, but the main idea is that they're a unique, human-readable way to identify some unique "thing". Their nature also offers the benefit of being unlikely to "collide" with one another.

The first thing we'll want to define is some kind of top level prefix to put on all our identifiers.

Fun Fact: Historically these identifiers were literally reversed-DNS. That is to say, to form one in code we might take our website's domain name, reverse it, then append our app's name (for example). These days, on Apple platforms, this isn't really a "thing" anymore. Now, we're usually given a string property or text field where we can supply just about any value we want.

This means that the common TLD's on the internet are also commonly used here. A company may begin all of their identifiers with com., while a non-profit may start theirs with org..

Next, we'll want to think of some way to group all of our identifiers, across all of our apps.

Often this is a domain-style version of our company or organization's name, or perhaps simply our own name, all lowercased, with no punctuation.

Pro Tip: Identifiers can contain numbers, letters, hyphens (-), and dot (.) characters. Hi! Bundle ID can have hyphens, not underscores. They're also case-sensitive. (Thanks to reader @pointum for the tips here!)

This gives us something like com.littlebites.

Great. Now we can apply this wherever we need to identify things.

Here are a few examples:

Applications and Extensions

com.littlebites.reader - An identifier for an imaginary app to read Bites. We might be tempted to just use com.littlebites.app or something generic, but this way we leave ourselves open to the possibility of making other apps in the future.

In-App Purchase Identifiers

With these, we want to think about the complete set of In-App Purchases we plan on offering and create groups within them if possible.

com.littlebites.reader.subscriptions.monthly, com.littlebites.reader.subscriptions.yearly - Here we have an imaginary subscription plan In-App Purchase.

We know we want to offer other types of In-App Purchases in the future, so we've "grouped" all subscription purchases under a subscriptions. segment. Then we're able to add a shorter phrase denoting the type as the final segment.

App Groups

App Groups help us share data between our apps or our app's extensions.

group.com.littlebites.cache, group.com.littlebites.user - Apple encourages to begin these with the group.. This way we know right away what we're dealing with. Here we've created two app groups, one to share less-important cache data (images, etc.) between apps while we sort of "wall-off" user data into it's own group.

That's all for now, have a tip or some advice about these identifiers that to share? Send it to hello@littlebitesofcocoa.com.

Topics

#316: Codable Dates 📠

Topics

In Bite 315 we started looking at the new Codable protocol in Swift 4. Today we'll learn how to work with Date types when encoding and decoding. Let's dive in.

We'll start with another simple struct:

struct Spaceship : Codable {
  var name: String
  var createdAt: Date
}

Then, we'll create one and encode it into JSON to see what it looks like:

let ship = Spaceship(
  name: "Skyhopper",
  createdAt: Date()
)

let encoder = JSONEncoder()

if let data = try? encoder.encode(ship) {
  print(String(data: data, encoding: .utf8)!)
}

This prints:

{
  "name":"Skyhopper",
  "createdAt":524735194.61138701
}

There's a lot more than meets the eye here though. That JSONEncoder we created has some incredibly helpful properties on it.

The one we want is .dateEncodingStrategy. Yep, it's a Swift enum with all sorts of goodies inside. Let's check them out.

We've already seen the default option, which is called .deferredToDate.

Another great one is .iso8601:

{
  "name":"Skyhopper",
  "createdAt":"2017-08-18T07:50:53Z"
}

Super neat to see that built in to the system in such a clean way.

Decoders have a similar property as well.

We've all had to decode some crazy date format from an HTTP API before. Now it's (hopefully) a little less painful.

Let's try decoding a funky format.

All we need to do is use the .formatted(DateFormatter) value for our JSONDecoder's .dateDecodingStrategy property.

We'll give it this JSON to decode:

{
  "name":"Skyhopper",
  "createdAt":"Friday, Aug 18, 2017"
}

Then, we'll set up our decoder and formatter:

let decoder = JSONDecoder()

let formatter = DateFormatter()
formatter.dateFormat = "EEEE, MMM d, yyyy"

decoder.dateDecodingStrategy = .formatted(formatter)
let data = jsonString.data(using: .utf8)!

try? decoder.decode(Spaceship.self, from: data)

Which decodes to our struct as expected:

Spaceship(
  name: "Skyhopper",
  createdAt: 2017-08-18 05:00:00 +0000
)

Neat!

That's all for now. Happy coding!

We're very happy to welcome back one of our favorite sponsors again this week. It's buddybuild!

Long time readers will remember that buddybuild is a fantastic mobile continuous integration, delivery, crash reporting and feedback platform, all in one.

Thousands of development teams, like Slack, Meetup and The Washington Post love buddybuild because it's the easiest way to build, test and distribute their mobile applications.

It takes just minutes to set up, let's check it out!

We'll head over to buddybuild.com and click Get Started.

We'll sign up with Github (we could also use Bitbucket, GitLab, or sign up with email / password for repos on any other git server). Neat!

Next, buddybuild will let us choose a git repo to create our first project with.

 

Once we select the repo we want to build, buddybuild will get to work building our app for the first time.

After that build is complete, our app is set up on buddybuild - it's that easy.

buddybuild installs and configures the necessary web hooks for our project so new builds are triggered whenever code is pushed. It also scans your repo for any tests you might have written, and runs them reliably on the simulator and physical devices.

 

With buddybuild, we won't need to wait for App Store processing time or reviews before deploying to testers.

buddybuild's continuous deployment system can send our app to users instantly on every build, on a schedule that works for our team, or at the push of a button.

This kind of comprehensive Continuous Integration and Continuous Deployment workflow is a game changer for iOS and Android teams. It also means we can have our whole development workflow set up in just minutes. We can also infinitely customize and extend buddybuild so it exactly just the way we want.

Buddybuild's deployment service also handles the process of adding new users (and their devices) by automatically managing UDIDs, iOS provisioning profiles and signing identities. (Fantastic UX for both testers and devs!)

 

Only buddybuild goes even farther, and gives us a fully integrated feedback platform.

Once users have installed our app and are testing it out, they can send their feedback (along with important diagnostic details) by simply taking a screenshot.

buddybuild also has rich pull request support. We can even pair it up with Danger to automate (the usually tedious) code review processes.

If our team works with pull requests frequently, buddybuild can be configured to build the pull request, then merge, commit, and update the commit status in GitHub, Bitbucket and GitLab. This ensures we always know before when a pull request is safe to merge.

If we have Unit Tests or UI Tests for our app, buddybuild will consistently and reliably run them as part of every build and automatically display how much of our codebase is covered by our tests. Neat!

Should a test fail, buddybuild will provide us with a video replay of our test's run and detailed test results to quickly understand the cause of the failure. (Learn more about testing on buddybuild here.)

If our app ever crashes, buddybuild's Source Context functionality will trace back and highlight the exact line of offending source code that caused the crash, telling us which users were affected, and how many times the crash happened.

Each crash is also recorded with an Instant Replay - this is a video replay of our users' actions in the moments leading up to the crash. Never wonder about "steps to reproduce" again! Super cool.

Buddybuild is also a great community citizen, and has our backs as iOS developers. For example, they'll give us a heads up on any potential breaking changes in Xcode. Within 48 hours of any Xcode release (including betas!), buddybuild will automatically takes our most recent successful build, and build and run tests against it using the latest version of Xcode. Then it will email us the results. How cool is that?!

Last but certainly not least, buddybuild not only comes with built-in integrations with tons of services we all know and love like GitHub, BitBucket, GitLab, Slack, JIRA Pivotal Tracker, Slack, HipChat but it's also infinitely customizable to meet the exact needs of your development team!

Buddybuild is the real deal, give them a try today at buddybuild.com!

Topics

#315: Codable Basics 📠

Topics

Swift 4 has brought with it a ton of great improvements. Today we'll cover one of them, it's the new Codable protocol! Codable ships with Swift 4 as part of the Swift standard library and allows us to conform our own types to it to get encoding and decoding functionality "for free".

Encoding and decoding types has never been easier.

Let's dive in!

Before we begin lets back up a second and look at what we're trying to accomplish when it comes to encoding and decoding.

Essentially we want to take an instance of an object or struct and convert (or "encode") it into some other, (usually "machine-readable") format. Then, later, we want to be able to convert (or "decode") the information in that format back into our object or struct.

These days the most common format for encoding/decoding is JSON. It's supported just about everywhere and has the advantage of being both human and machine readable.

For this reason, we'll be discussing JSON in this Bite. It's important to note though, that Codable isn't specific to just JSON. It's built to support encoding and decoding into just about any format we can think of.

In the past, we might have used something like the JSONSerialization class in Foundation to do encode our types into JSON.

This works great, but the encoding input and deocding output was either a Dictionary<String: Any>, or an Array of them. Then we'd need to manually convert to/from our "real" types.

With Codable, we can do much better!

Let's start with a simple struct conforming to Codable:

struct Spaceship : Codable {
  var name: String
}

let ship = Spaceship(name: "Skyhopper")

Now, all we need to do to convert our ship to JSON is:

let data = try? JSONEncoder().encode(ship)

This returns a Data that we can send to a server, store to disk, or do anything else we need with.

Let's convert it to a string real quick so we can take a look at it:

if let string = String(data: data, encoding: .utf8) {
  print(string)
}

This prints:

{"name":"Skyhopper"}

Nice! Notice how we didn't need to specify the names of the "keys" in the JSON, they are inferred from the property names on our struct.

For decoding, we can easily go the other way:

let ship = try? JSONDecoder().decode(Spaceship.self, from: data)

Note how both the .encode and .decode functions can throw and thus we're using the try? keyword to return a nil Optional if encoding or decoding fails.

Before we go let's cover one more nice thing about Codable.

Let's add another struct to the mix:

struct Person : Codable {
  var firstName: String
  var lastName: String
}

struct Spaceship : Codable {
  var name: String
  var pilot: Person
}

Nice. Now when we encode a Spaceship, the system notices that the pilot property's value is also Codable and does the right thing for us automatically:

{"name":"Skyhopper", "pilot":{"firstName":"Luke", "lastName":"Skywalker"}}

That's all for today, next time we'll look at using Codable with more complex types of data likes Dates. Happy coding!

This week we're welcoming back one of our favorite sponsors: It's buddybuild!

Long time readers will remember that buddybuild is a fantastic mobile continuous integration, delivery, crash reporting and feedback platform, all in one.

Thousands of development teams, like Slack, Meetup and The Washington Post love buddybuild because it's the easiest way to build, test and distribute their mobile applications.

It takes just minutes to set up, let's check it out!

We'll head over to buddybuild.com and click Get Started.

We'll sign up with Github (we could also use Bitbucket, GitLab, or sign up with email / password for repos on any other git server). Neat!

Next, buddybuild will let us choose a git repo to create our first project with.

 

Once we select the repo we want to build, buddybuild will get to work building our app for the first time.

After that build is complete, our app is set up on buddybuild - it's that easy.

buddybuild installs and configures the necessary web hooks for our project so new builds are triggered whenever code is pushed. It also scans your repo for any tests you might have written, and runs them reliably on the simulator and physical devices.

 

With buddybuild, we won't need to wait for App Store processing time or reviews before deploying to testers.

buddybuild's continuous deployment system can send our app to users instantly on every build, on a schedule that works for our team, or at the push of a button.

This kind of comprehensive Continuous Integration and Continuous Deployment workflow is a game changer for iOS and Android teams. It also means we can have our whole development workflow set up in just minutes. We can also infinitely customize and extend buddybuild so it exactly just the way we want.

Buddybuild's deployment service also handles the process of adding new users (and their devices) by automatically managing UDIDs, iOS provisioning profiles and signing identities. (Fantastic UX for both testers and devs!)

 

Only buddybuild goes even farther, and gives us a fully integrated feedback platform.

Once users have installed our app and are testing it out, they can send their feedback (along with important diagnostic details) by simply taking a screenshot.

buddybuild also has rich pull request support. We can even pair it up with Danger to automate (the usually tedious) code review processes.

If our team works with pull requests frequently, buddybuild can be configured to build the pull request, then merge, commit, and update the commit status in GitHub, Bitbucket and GitLab. This ensures we always know before when a pull request is safe to merge.

If we have Unit Tests or UI Tests for our app, buddybuild will consistently and reliably run them as part of every build and automatically display how much of our codebase is covered by our tests. Neat!

Should a test fail, buddybuild will provide us with a video replay of our test's run and detailed test results to quickly understand the cause of the failure. (Learn more about testing on buddybuild here.)

If our app ever crashes, buddybuild's Source Context functionality will trace back and highlight the exact line of offending source code that caused the crash, telling us which users were affected, and how many times the crash happened.

Each crash is also recorded with an Instant Replay - this is a video replay of our users' actions in the moments leading up to the crash. Never wonder about "steps to reproduce" again! Super cool.

Buddybuild is also a great community citizen, and has our backs as iOS developers. For example, they'll give us a heads up on any potential breaking changes in Xcode. Within 48 hours of any Xcode release (including betas!), buddybuild will automatically takes our most recent successful build, and build and run tests against it using the latest version of Xcode. Then it will email us the results. How cool is that?!

Last but certainly not least, buddybuild not only comes with built-in integrations with tons of services we all know and love like GitHub, BitBucket, GitLab, Slack, JIRA Pivotal Tracker, Slack, HipChat but it's also infinitely customizable to meet the exact needs of your development team!

Buddybuild is the real deal, give them a try today at buddybuild.com!

This time of year it's common to have multiple versions of Xcode installed.

Today we're going to learn how to tell our system which version of Xcode's tools to use when working with Xcode from the command line. But first we'll check out a helpful tool to actually install Xcode from command line.

Let's dive in.

The first tool we'll look at can be used to install Xcode versions directly from the command line. It's an alternative to using the Mac App Store (or just managing downloads manually).

It's called xcode-install and it can be found on Github right here.

We can install it with:

gem install xcode-install`

Once that's finished, we can list the versions of Xcode that are available to install from the command line like this:

xcversion list

Which at the time of this writing will print:

8
8.1
8.2
8.2.1 (installed)
8.3 (installed)
8.3.1
8.3.2
8.3.3 (installed)
9 beta 4

By default, xcode-install only prints the last few major versions.

(Sidenote: For fun and nostalgia try running xcversion list --all to print all the available versions of Xcode going all the way back to 4.3 for OS X Lion 😱).

We can install a version like this:

xcversion install "9 beta 4"

We'll be prompted for our Apple Developer credentials which will be stored in our Keychain, and then the version will be download, installed, and moved into place, all without leaving the command line.

Neat!

More info about xcode-install can be found here on Github.

Next we'll need a way to switch to the new version we just installed.

The tool we'll be working with is already installed on our Mac and is called xcode-select.

It's a straightforward, single-purpose utility that essentially controls which path on disk gets run when we run xcrun, xcodebuild, etc. from our command line.

Let's first check out which version of Xcode we're currently using:

xcode-select --print-path

Which (by default) will print:

/Applications/Xcode.app/Contents/Developer

(Note: This is what's printed for versions of Xcode installed from the App Store).

Next, lets take a look at all of the versions of Xcode we currently have installed.

Here we're going to grep for Xcode inside of our /Applications directory:

ls /Applications | grep Xcode

Which (for example) will print:

Xcode-7.3.1.app
Xcode-8.2.1.app
Xcode-8.app
Xcode-9-beta-4.app
Xcode.app

All we need to do to override the default Xcode path is pass the path of one of these Xcode directories in with the --switch flag:

sudo xcode-select --switch /Applications/Xcode-9-beta-4.app

We'll need the sudo since this xcode-select is working on a system-wide level.

Also, note how we've omitted the /Contents/Developer bit from the path, xcode-select will infer that for us).

Now when we run xcrun, xcodebuild, etc. from our command line we'll be using the Xcode 9 Beta version of each tool.

Nice.

Today we're going to take a look at how we can control (and interact with) the iOS Simulator from the command line.

There's no need to install anything new though. The tool we're going to be using is already on our Mac, hiding inside the xcrun command, which gets installed with Xcode.

It's called simctl.

Let's begin!

First thing we'll want to do is tell simctl to print out a list of our current iOS Simulator devices. We can do that by running:

xcrun simctl list devices

This will print a list of all the simulated devices and a UDID value with each:

-- iOS 10.3 --
iPhone 7 (C4481459-5BB1-4CE1-9BE0-CF0FEA351299) (Booted)
iPhone 7 Plus (ADCB6F99-5ADD-49B1-83AE-5391D845C4D0) (Shutdown)
iPhone SE (FB0899C0-5812-492E-80D9-9DE517554C12) (Shutdown)
iPad Pro (9.7 inch) (45F47977-9A03-4DD1-8FD0-289F7936FE98) (Shutdown)
iPad Pro (10.5-inch) (C3C909DC-BF70-4D67-BF7E-A41A0CF4AF56) (Shutdown)
...

We can see by the (Booted) notation next to the first device that simctl has identified the iOS Simulator we had open at the time of running the command.

We can pass that UDID (C4481459-5BB1-4CE1-9BE0-CF0FEA351299) to other commands to target our currently running iOS Simulator.

For shorthand though, we can also just pass the term booted to target the currently running iOS Simulator.

(Note that in Xcode 9 multiple iOS Simulators can be running at once. If we pass booted in this case, simctl will just choose one for us).

Whew. Ok with that introduction out of the way let's try this thing out.

First up, one of the best features of simctl, opening URLs.

To open a URL in the iOS Simulator, from the command line, all we need to run is:

xcrun simctl openurl booted "https://littlebitesofcocoa.com"

If we look at our iOS Simulator after running that, we'll see Safari open up and load the page.

Neat!

Even better, this works just as well with custom URL schemes:

xcrun simctl openurl booted "spaceships://ships/123"

Very cool.

Next, let's add some photos and videos.

We can use the addmedia subcommand to import media to the iOS Simulator:

xcrun simctl addmedia booted ~/Desktop/images/image1.png ~/Desktop/images/image2.jpg ~/Desktop/images/image3.jpg

We can include one or more file paths here. It supports images, videos, and even Live Photos.

After running, the files will be imported and appear in the Photo Library:

Next: iCloud Syncing.

We can explicitly force a sync of iCloud using this command:

xcrun simctl icloud_sync booted

What's nice about this is we'll even get errors printed if (for example) iCloud isn't yet configured in this simulated device:

An error was encountered processing the command (domain=BRCloudDocsErrorDomain, code=2):
The operation couldn’t be completed.
(BRCloudDocsErrorDomain error 2 - Logged out - iCloud Drive is not configured)

This next one's a doozy. We can use simctl to record and stream live video, and capture screenshots of any screen of our iOS Simulator.

First let's grab a screenshot:

xcrun simctl io booted screenshot ~/Desktop/screenshot.png

Neat. Easy enough, next let's try recording a movie:

xcrun simctl io booted recordVideo --type=mp4 ~/Desktop/movie.mp4

Full quality movie file is available here, for the curious.

The recordVideo subcommand's output can be | (piped) into other commands or even to a TCP or UDP socket for live streaming. This works on all iOS, tvOS, and even watchOS Simulators. Very cool.

Last but not least, we can print out the path for app's installation directory on disk:

xcrun simctl get_app_container booted com.spaceships.app
/Users/example/Library/Developer/CoreSimulator/Devices/C4481459-5BB1-4CE1-9BE0-CF0FEA351299/data/Containers/Bundle/Application/49A59051-1404-431C-8B65-B589EC0F6267/Spaceships.app

Nice.

To save us a step, we can | (pipe) that path output to pbcopy to put the value on our clipboard for easy pasting later:

xcrun simctl get_app_container booted com.spaceships.app | pbcopy

There's plenty more that simctl can do. To see the long list of available commands and features, we can run:

xcrun simctl

We can add, remove and even reset/erase simulated devices, interact with a device's pasteboards, launch apps with environment variables and much more.

That's all for now, happy simulating!

-- Update: Tuesday July 25th, 2017 --

Friend of the pod Bites? (and creator of the fantastic Fastlane suite of tools, (Bites)) Felix Krause writes in with a great addition regarding the --json flag:

We can pass the --json (-j for shorthand) flag to any of the simctl commands that print information to print it in JSON format.

This is perfect for | (pipe)-ing to other commands, or for consumption by a host process/program:

xcrun simctl list runtimes --json
{
  "runtimes" : [
    {
      "buildversion" : "14E8301",
      "availability" : "(available)",
      "name" : "iOS 10.3",
      "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-10-3",
      "version" : "10.3.1"
    },
    {
      "buildversion" : "14V243",
      "availability" : "(available)",
      "name" : "watchOS 3.2",
      "identifier" : "com.apple.CoreSimulator.SimRuntime.watchOS-3-2",
      "version" : "3.2"
    }
  ]
}

Topics

#312: Asset Catalog Improvements 🎨

Topics

Asset Catalogs have been around for a few Xcode releases. They're a great way for us to organize and configure graphical assets (among many other things) for our app. Today we'll check out the improvements to Asset Catalogs in Xcode 9. Let's dive in.

First up, colors.

Yep, we can now define and organize named colors inside of an Asset Catalog!

We can select "New Color Set", and then use all of the normal features of Asset Catalogs, including new Wide Gamut support.

Then, in our code we can reference these colors like this:

view.backgroundColor = UIColor(named: "wood")

Neat!

The second big addition we're going to look at is "real" vector-based asset support.

In past Xcode releases, we were able to add image assets to our catalogs with a format of PDF. This worked great, but under the hood, Xcode would simply render our asset at the @1x, @2x, and @3x sizes and save non-vector (i.e. png images) into our app's bundle.

In Xcode 9 however, we're given a beautiful new checkbox called "Preserve Vector Data".

This means that if our image is loaded in our code, and we ask it to display at a a larger size than it is by default, it will be scaled up at runtime by the system.

This means we can now write code like this without any quality loss at render time:

let normal  = UIImageView(image: UIImage(named: "cocoapod"))
normal.tintColor = UIColor(named: "covfefe")

view.addSubview(normal)

let big  = UIImageView(image: UIImage(named: "cocoapod"))
big.tintColor = UIColor(named: "cream")
big.frame = CGRect(x: 50, y: 200, width: normal.bounds.size.width * 2, height: normal.bounds.size.height * 2)

view.addSubview(big)

Very, very cool.

Weekly Sponsor: Zendesk 💡

We're welcoming back one of our favorite sponsors this week, it's Zendesk!

None of us have ever built a flawless app.

Chances are, no matter how good we think our app's user experience is, there's probably something users will need help with while they're in our app.

Yet in many apps, getting help is reduced to a "contact us" button that launches an compose email screen, or a just drops the user on a web page.

By forcing folks out of the app to get help, small problems can turn into big annoyances, and those will almost certainly turn into negative App Store reviews and poor ratings.

With Zendesk's Mobile SDKs, we can join the makers of Angry Birds, Venmo and Swiftkey in bringing native, in-app support functionality to our apps quickly and easily.

Our users can view help and support content, and even submit tickets to support without ever leaving our app. Neat!

Tickets go into Zendesk and can include technical details about the user and their device, history with our apps, and more.

Best of all, it's included with Zendesk at no extra charge.

We can use Zendesk's "out-of-the-box" iOS UI components to get up and running quickly, or we can build your own UI with SDK API Providers.

A huge thanks to Zendesk for sponsoring!

Topics

#311: Round Corner Improvements ✅

Topics

Today we're going to talk about corners. Specifically, rounded ones. In iOS 11, Apple has improved the way we can specify and work with the rounding of our views' corners. Let's take a look.

Let's begin with where we were before iOS 11.

Before iOS 11, configuring a UIView to have round corners went like this:

let view = UIView()

view.clipsToBounds = true
view.layer.cornerRadius = 8

The clipsToBounds ensures the "clipping" takes place, and the next rounds all four corners with a radius of 8:

Pretty straightforward.

This approach works great for rounding all four corners of a view. But what if we wanted to round only some of the four corners? Previously we'd need to drop down and create our own mask layer, giving it a path we created manually. Bummer.

iOS 11 makes this way easier.

Meet CACornerMask!

Now, we can use the same code as before, with one small addition:

let view = UIView()

view.clipsToBounds = true
view.layer.cornerRadius = 8
view.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]

Now we can use the new CACornerMask option set to specify which corners we'd like rounded. Here we've asked for just the bottom two corners to be rounded with an 8 point radius:

Neat!

Before we go, there's one last thing to know about round corners in iOS 11:

They are now completely animatable! 🎉

In previous releases, this next bit of code wouldn't do anything, but in iOS 11 this works exactly as expected:

let view = UIView()

view.clipsToBounds = true
view.layer.cornerRadius = 16

UIViewPropertyAnimator(duration: 1, curve: .linear) {
  view.layer.cornerRadius = 0
}.startAnimation()

Very cool.

Page 1 of 38