title | author | category | excerpt |
---|---|---|---|
iOS 8 |
Mattt Thompson |
Ask anyone, and they'll tell you: WWDC 2014 was the one of the most exciting in recent memory. This week, we'll take a look beneath the headline features, and share some of the more obscure APIs that everyone should know about. |
Ask anyone, and they'll tell you: WWDC 2014 was one of the most exciting in recent memory. It was, first and foremost, a developer event, with nary a hardware announcement to upstage the latest software & developer tools.
And boy howdy, was there a lot to be excited about.
The announcements from iOS 8 & OS X Yosemite alone would have made 2014 a bellwether year for the Apple platform, with Extensions, Continuity, SpriteKit enhancements, SceneKit for iOS, Metal, Game HealthKit, HomeKit, Local Authentication, and a brand new Photos framework. Not to mention the dramatic improvements to Xcode & Interface Builder, a revamped iTunes Connect, TestFlight, Crash Reports, and CloudKit. And oh yeah—Swift.
The kicker? Apple has graciously relaxed its NDA for new technologies, meaning that we don't have to wait to talk about all of the shiny new toys we have to play with.
This week, we'll take a look beneath the headline features, and share some of the more obscure APIs that everyone should know about.
From here on out, NSHipster will primarily write code samples in Swift, with the occasional Objective-C throwback where appropriate. By the end of the summer, we hope to have all of the existing code samples ported to Swift, with the option to toggle between languages.
Forget [[UIDevice currentDevice] systemVersion]
and NSFoundationVersionNumber
, there's a new way to determine the current operating system in code: NSProcessInfo -isOperatingSystemAtLeastVersion
import Foundation
let yosemite = NSOperatingSystemVersion(majorVersion: 10, minorVersion: 10, patchVersion: 0)
NSProcessInfo().isOperatingSystemAtLeastVersion(yosemite) // false
Keep in mind, however, that a test for capability, such as with SomeClass.class
or respondsToSelector:
, is preferable to checking the OS version. Compiler macros in C or Swift can be used to conditionally compile source based on the build configuration of the target.
One of the features most sorely lacking in Foundation was the ability to work with units for quantities like mass or length. In iOS 8 and OS X Yosemite, three new classes were introduced that fills the gap: NSEnergyFormatter
, NSMassFormatter
, & NSLengthFormatter
.
This effectively doubles the number of
NSFormatter
subclasses in Foundation, which was previously limited toNSNumberFormatter
,NSDateFormatter
, &NSByteCountFormatter
.
Although these new formatter classes are part of Foundation, they were added primarily for use in HealthKit.
NSEnergyFormatter
formats energy in Joules, the raw unit of work for exercises, and Calories, which is used when working with nutrition information.
let energyFormatter = NSEnergyFormatter()
energyFormatter.forFoodEnergyUse = true
let joules = 10_000.0
println(energyFormatter.stringFromJoules(joules)) // "2.39 Cal"
Although the fundamental unit of physical existence, mass is pretty much relegated to tracking the weight of users in HealthKit. Yes, mass and weight are different, but this is programming, not science class, so stop being pedantic.
let massFormatter = NSMassFormatter()
let kilograms = 60.0
println(massFormatter.stringFromKilograms(kilograms)) // "132 lb"
Rounding out the new NSFormatter
subclasses is NSLengthFormatter
. Think of it as a more useful version of MKDistanceFormatter
, with more unit options and formatting options.
let lengthFormatter = NSLengthFormatter()
let meters = 5_000.0
println(lengthFormatter.stringFromMeters(meters)) // "3.107 mi"
Continuing on iOS 8's health kick, CMStepCounter
is revamped in the latest release. CMPedometer
is a strict improvement over its predecessor, with the ability to query from discrete points in time, track both steps and distance, and even calculate how many flights of stairs were climbed.
It's amazing what that M7 chip is capable of.
import CoreMotion
let lengthFormatter = NSLengthFormatter()
let pedometer = CMPedometer()
pedometer.startPedometerUpdatesFromDate(NSDate(), withHandler: { data, error in
if !error {
println("Steps Taken: \(data.numberOfSteps)")
let distance = data.distance.doubleValue
println("Distance: \(lengthFormatter.stringFromMeters(distance))")
let time = data.endDate.timeIntervalSinceDate(data.startDate)
let speed = distance / time
println("Speed: \(lengthFormatter.stringFromMeters(speed)) / s")
}
})
On supported devices, a CMPedometer
's stats on floorsAscended
/ floorsDescended
can be augmented with CMAltimeter
to get a more granular look at vertical distance traveled:
import CoreMotion
let altimeter = CMAltimeter()
if CMAltimeter.isRelativeAltitudeAvailable() {
altimeter.startRelativeAltitudeUpdatesToQueue(NSOperationQueue.mainQueue(), withHandler: { data, error in
if !error {
println("Relative Altitude: \(data.relativeAltitude)")
}
})
}
CLFloor
is a new API in iOS 8 that ties the new features in CoreMotion with Apple's ambitious plan to map the interiors of the largest buildings in the world. Look for this information to play a significant role in future hyperlocal mapping applications.
import CoreLocation
class LocationManagerDelegate: NSObject, CLLocationManagerDelegate {
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: AnyObject[]!) {
let location: CLLocation? = locations[0] as? CLLocation
if let floor: CLFloor? = location?.floor {
println("Current Floor: \(floor?.level)")
}
}
}
let manager = CLLocationManager()
manager.delegate = LocationManagerDelegate()
manager.startUpdatingLocation()
As a framework, HealthKit covers a lot of ground, with dozens of new classes and constants. A good place to start, in terms of understanding what's possible is HKStatistics
.
HealthKit manages your biometrics from all of your devices in a single unified API. Statistics on things like heart rate, caloric intake, and aerobic output can be tracked and aggregated in powerful ways.
The following example shows how statistics summed over the duration of the day can be grouped and interpreted individually:
import HealthKit
let collection: HKStatisticsCollection? = ...
let statistics: HKStatistics? = collection!.statisticsForDate(NSDate())
for item: AnyObject in statistics!.sources {
if let source = item as? HKSource {
if let quantity: HKQuantity = statistics!.sumQuantityForSource(source) {
if quantity.isCompatibleWithUnit(HKUnit.gramUnitWithMetricPrefix(.Kilo)) {
let massFormatter = NSMassFormatter()
let kilograms = quantity.doubleValueForUnit(HKUnit.gramUnitWithMetricPrefix(.Kilo))
println(massFormatter.stringFromKilograms(kilograms))
}
if quantity.isCompatibleWithUnit(HKUnit.meterUnit()) {
let lengthFormatter = NSLengthFormatter()
let meters = quantity.doubleValueForUnit(HKUnit.meterUnit())
println(lengthFormatter.stringFromMeters(meters))
}
if quantity.isCompatibleWithUnit(HKUnit.jouleUnit()) {
let energyFormatter = NSEnergyFormatter()
let joules = quantity.doubleValueForUnit(HKUnit.jouleUnit())
println(energyFormatter.stringFromJoules(joules))
}
}
}
}
NSHipster will be covering a lot more about HealthKit in future editions, so stay tuned!
In many ways, WWDC 2014 was the year that Apple fixed their shit. Small things, like adding the missing NSStream
initializer for creating a bound stream pair (without resorting to awkwardly-bridged CFStreamCreatePairWithSocketToHost
call). Behold: +[NSStream getStreamsToHostWithName:port:inputStream:outputStream:]
var inputStream: NSInputStream?
var outputStream: NSOutputStream?
NSStream.getStreamsToHostWithName("nshipster.com",
port: 5432,
inputStream: &inputStream,
outputStream: &outputStream)
Also filed under: "small but solid fixes", is this convenience method for NSString
:
let string: NSString = "Café"
let substring: NSString = "É"
string.localizedCaseInsensitiveContainsString(substring) // true
If you're a linguistics and typography nerd, this new addition to the CoreText framework may have you standing up on your chair and cheering. "What's with Jim? Is it me, or has he been acting kind of weird since his trip to San Francisco?", they'll say, looking at you atop your desk as you tear your clothes off your body in a frenzy of pure ecstasy. "Yeah, remind me not to stay in the Tenderloin for next year's conference."
...oh right. Ruby. No, not Ruby. Ruby. It's used to display the pronunciation of characters in certain Asian scripts.
@import CoreText;
NSString *kanji = @"猫";
NSString *hiragana = @"ねこ";
CFStringRef furigana[kCTRubyPositionCount] =
{(__bridge CFStringRef)hiragana, NULL, NULL, NULL};
CTRubyAnnotationRef ruby =
CTRubyAnnotationCreate(kCTRubyAlignmentAuto, kCTRubyOverhangAuto, 0.5, furigana);
Admittedly, the documentation isn't entirely clear on how exactly to incorporate this into the rest of your CoreText
drawing calls, but the result would look something like this:
What's even nerdier than Ruby annotations? The new calendar identifiers added to iOS 8 & OS X Yosemite. This update brings Foundation up to the latest version of the CLDR:
(Sadly, the French Republican Calendar is still but a twinkle in the eyes of NSHipsters everywhere)
NSCalendarIdentifierCoptic
: a.k.a Alexandrian calendar, is used by the Coptic Orthodox Church.NSCalendarIdentifierEthiopicAmeteMihret
: Ethiopic calendar, Amete Mihret (epoch approx. 8 C.E.)NSCalendarIdentifierEthiopicAmeteAlem
: Ethiopic calendar, Amete Alem (epoch approx. 5493 B.C.E.)NSCalendarIdentifierIslamicTabular
: A simple tabular Islamic calendar using the astronomical/Thursday epoch of CE 622 July 15.NSCalendarIdentifierIslamicUmmAlQura
: The Islamic Umm al-Qura calendar used in Saudi Arabia. This is based on astronomical calculation, instead of tabular behavior.
The Foundation URL Loading System has remained relatively unchanged since last year's NSURLSession
blowout. However, NSURLCredentialStorage
has been given some TLC, with new functions that get and set credentials for tasks in asynchronous, non-blocking fashion.
import Foundation
let session = NSURLSession()
let task = session.dataTaskWithURL(NSURL(string: "http://nshipster.com"), completionHandler: { data, response, error in
// ...
})
let protectionSpace = NSURLProtectionSpace()
NSURLCredentialStorage.getCredentialsForProtectionSpace(protectionSpace: protectionSpace, task: task, completionHandler: { credentials in
// ...
})
Looking through the latest API diffs, one might notice the large number of new UTIs constants. One that caught my eye was kUTTypeToDoItem
:
import MobileCoreServices
kUTTypeToDoItem // "public.to-do-item"
As a public type, iOS & OS X now provide a unified way to share tasks between applications. If you happen to work on a task management tool (and, let's be honest, the chances are extremely good, considering how damn many of them there are in the App Store), proper integration with this system type should be put at the top of your list.
Most users are completely unaware that most pictures taken with phones these days include GPS metadata. Countless individuals have had their privacy breached because of this small detail.
New to the Image I/O framework is a convenient new option for CGImageDestination
: kCGImageMetadataShouldExcludeGPS
, which does what you'd expect.
@import UIKit;
@import ImageIO;
@import MobileCoreServices;
UIImage *image = ...;
NSURL *fileURL = [NSURL fileURLWithPath:@"/path/to/output.jpg"];
NSString *UTI = kUTTypeJPEG;
NSDictionary *options = @{
(__bridge id)kCGImageDestinationLossyCompressionQuality: @(0.75),
(__bridge id)kCGImageMetadataShouldExcludeGPS: @(YES),
};
CGImageDestinationRef imageDestinationRef =
CGImageDestinationCreateWithURL((__bridge CFURLRef)fileURL,
(__bridge CFStringRef)UTI,
1,
NULL);
CGImageDestinationAddImage(imageDestinationRef, [image CGImage], (__bridge CFDictionaryRef)options);
CGImageDestinationFinalize(imageDestinationRef);
CFRelease(imageDestinationRef);
#define WTF_PLATFORM_IOS
has been removed from JavaScriptCore. It will be missed.
UIWebView
is dead. Long live WKWebView
.
WKWebView
offers Safari-level performance to your own app, and further improves on UIWebView
with preferences and configurations:
import WebKit
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = false
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
let webView = WKWebView(frame: self.view.bounds, configuration: configuration)
let request = NSURLRequest(URL: NSURL(string: "http://nshipster.com"))
webView.loadRequest(request)
Threads have been systematically de-emphasized from the conceptual foundation of Apple frameworks. This has been a good thing for developers.
Following this trend is a change to NSOperation
in the latest APIs. A new qualityOfService
property replaces the threadPriority
. These new semantics allow an app to defer non-critical work to ensure a consistently great user experience.
The NSQualityOfService
enum defines the following values:
UserInteractive
: UserInteractive QoS is used when performing work that is related to graphically intensive work such as scrolling or animating.UserInitiated
: UserInitiated QoS is used for performing work that has been explicitly requested by the user, but does not require millisecond accuracy like animations. For example, if a user requests an email app to check for mail right now.Utility
: Utility QoS is used for performing work that has been requested by the user to happen automatically. For example, an email app may be configured to automatically check for mail every 5 minutes. It is not a problem if the email check is deferred by a few minutes if the system is extremely limited in resources.Background
: Background QoS is used for performing work that the user may not even be aware is happening on their behalf. For example, an email app may use this to perform indexing for a search.
Quality of Service is used throughout Foundation in iOS 8 & OS X Yosemite, so be on the lookout for opportunities to capitalize on this new feature.
Finally, one of the most anticipated features of iOS 8: LocalAuthentication. Ever since TouchID was introduced with the iPhone 5s, developers have been salivating at the prospect of using that in their own app.
Imagine: with CloudKit and LocalAuthentication, nearly all of the friction to creating a user account is gone. Just scan your fingerprint, and you're in.
LocalAuthentication works in terms of an LAContext
class, which evaluates a specified policy, and gives a thumbs up or thumbs down on user authentication. At no point is any biometric information made available to the application—everything is kept safe on the hardware itself.
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
error:&error])
{
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:NSLocalizedString(@"...", nil)
reply:^(BOOL success, NSError *error) {
if (success) {
// ...
} else {
NSLog(@"%@", error);
}
}];
} else {
NSLog(@"%@", error);
}
Although it seems like all that anyone can talk about these days is Swift, it'd be a shame if we ignored all of the neat things iOS 8 & OS X Yosemite allow us to actually do with this new language.
If you're feeling adventurous, dive into the iOS 7.1 to 8.0 API diffs to really appreciate the magnitude of new technologies to discover. Granted, of the 4000+ new APIs, at least half of those are slight changes to Accelerate functions, or methods becoming properties, but still... Have at it!