228 A Peek at 3D Touch 03 A FINAL › videos › wwdc › 2016 › 228a...Supporting 3D Touch...

Post on 04-Jul-2020

0 views 0 download

Transcript of 228 A Peek at 3D Touch 03 A FINAL › videos › wwdc › 2016 › 228a...Supporting 3D Touch...

© 2016 Apple Inc. All rights reserved. Redistribution or public display not permitted without written permission from Apple.

Enhancing your apps for the next dimension of touch

App Frameworks #WWDC16

Session 228

A Peek at 3D Touch

Tyler Fox UIKit Frameworks EngineerPeter Hajas UIKit Frameworks Engineer

Agenda

Agenda

Overview of 3D Touch

Agenda

Overview of 3D TouchHome Screen Quick Actions

Agenda

Overview of 3D TouchHome Screen Quick ActionsPeek and Pop

Agenda

Overview of 3D TouchHome Screen Quick ActionsPeek and PopUIPreviewInteraction

NEW

Tour across the systemOverview of 3D Touch

647 x 1150

647 x 1150

647 x 1150

647 x 1150

647 x 1150

647 x 1150

Supporting 3D Touch

Supporting 3D Touch

Accelerate access to existing features in your app

Supporting 3D Touch

Accelerate access to existing features in your appEnable new immersive interactions

Supporting 3D Touch

Accelerate access to existing features in your appEnable new immersive interactionsProvide a consistent experience across iOS

Leap straight into action from the home screenHome Screen Quick Actions

647 x 1150

647 x 1150

647 x 1150

647 x 1150

647 x 1150

647 x 1150

Home Screen Quick Actions

StaticDynamic

Two types

Home Screen Quick ActionsStatic

Home Screen Quick Actions

Defined in your app’s Info.plist

Static

Home Screen Quick Actions

Defined in your app’s Info.plistAvailable as soon as your app has been installed

Static

Home Screen Quick Actions

Defined in your app’s Info.plistAvailable as soon as your app has been installed

Static

DynamicHome Screen Quick Actions

DynamicHome Screen Quick Actions

Created by your app at runtime

DynamicHome Screen Quick Actions

Created by your app at runtimeAvailable after the first launch of your app

DynamicHome Screen Quick Actions

Created by your app at runtimeAvailable after the first launch of your appShown after any static quick actions (space permitting)

DynamicHome Screen Quick Actions

Created by your app at runtimeAvailable after the first launch of your appShown after any static quick actions (space permitting)Can include a system icon, custom icon, or Address Book contact

Using a contact for the iconDynamic Quick Actions

let contactName = "Lexi Torres"

var contactIcon: UIApplicationShortcutIcon? = nil

Using a contact for the iconDynamic Quick Actions

let contactName = "Lexi Torres"

var contactIcon: UIApplicationShortcutIcon? = nil

// Make sure to request access to the user's contacts first

if CNContactStore.authorizationStatus(for: .contacts) == .authorized {

}

Using a contact for the iconDynamic Quick Actions

let contactName = "Lexi Torres"

var contactIcon: UIApplicationShortcutIcon? = nil

// Make sure to request access to the user's contacts first

if CNContactStore.authorizationStatus(for: .contacts) == .authorized {

let predicate = CNContact.predicateForContacts(matchingName: contactName)

let contacts = try? CNContactStore().unifiedContacts(matching: predicate, keysToFetch: [])

}

Using a contact for the iconDynamic Quick Actions

let contactName = "Lexi Torres"

var contactIcon: UIApplicationShortcutIcon? = nil

// Make sure to request access to the user's contacts first

if CNContactStore.authorizationStatus(for: .contacts) == .authorized {

let predicate = CNContact.predicateForContacts(matchingName: contactName)

let contacts = try? CNContactStore().unifiedContacts(matching: predicate, keysToFetch: [])

if let contact = contacts?.first {

contactIcon = UIApplicationShortcutIcon(contact: contact)

}

}

Using a contact for the iconDynamic Quick Actions

let contactName = "Lexi Torres"

var contactIcon: UIApplicationShortcutIcon? = nil

// Make sure to request access to the user's contacts first

if CNContactStore.authorizationStatus(for: .contacts) == .authorized {

let predicate = CNContact.predicateForContacts(matchingName: contactName)

let contacts = try? CNContactStore().unifiedContacts(matching: predicate, keysToFetch: [])

if let contact = contacts?.first {

contactIcon = UIApplicationShortcutIcon(contact: contact)

}

}

let icon = contactIcon ?? UIApplicationShortcutIcon(type: .message)

let contactName = "Lexi Torres"

var contactIcon: UIApplicationShortcutIcon? = nil

// Make sure to request access to the user's contacts first

if CNContactStore.authorizationStatus(for: .contacts) == .authorized {

let predicate = CNContact.predicateForContacts(matchingName: contactName)

let contacts = try? CNContactStore().unifiedContacts(matching: predicate, keysToFetch: [])

if let contact = contacts?.first {

contactIcon = UIApplicationShortcutIcon(contact: contact)

}

}

let icon = contactIcon ?? UIApplicationShortcutIcon(type: .message)

Creating and registeringDynamic Quick Actions

// Create a dynamic quick action using the icon

let type = "com.company.app.sendChatTo"

let subtitle = "Send a chat"

let shortcutItem1 = UIApplicationShortcutItem(type: type, localizedTitle: contactName,

localizedSubtitle: subtitle, icon: icon)

Creating and registeringDynamic Quick Actions

// Create a dynamic quick action using the icon

let type = "com.company.app.sendChatTo"

let subtitle = "Send a chat"

let shortcutItem1 = UIApplicationShortcutItem(type: type, localizedTitle: contactName,

localizedSubtitle: subtitle, icon: icon)

// Repeat as needed for any additional dynamic quick actions...

Creating and registeringDynamic Quick Actions

// Create a dynamic quick action using the icon

let type = "com.company.app.sendChatTo"

let subtitle = "Send a chat"

let shortcutItem1 = UIApplicationShortcutItem(type: type, localizedTitle: contactName,

localizedSubtitle: subtitle, icon: icon)

// Repeat as needed for any additional dynamic quick actions...

let shortcutItems = [shortcutItem1, shortcutItem2, shortcutItem3]

Creating and registeringDynamic Quick Actions

// Create a dynamic quick action using the icon

let type = "com.company.app.sendChatTo"

let subtitle = "Send a chat"

let shortcutItem1 = UIApplicationShortcutItem(type: type, localizedTitle: contactName,

localizedSubtitle: subtitle, icon: icon)

// Repeat as needed for any additional dynamic quick actions...

let shortcutItems = [shortcutItem1, shortcutItem2, shortcutItem3]

// Register the dynamic quick actions to display on the home screen

application.shortcutItems = shortcutItems

647 x 1150

On app activationHandling Quick Actions

func application(application: UIApplication,

performActionForShortcutItem shortcutItem: UIApplicationShortcutItem,

completionHandler: Bool -> Void) {

}

On app activationHandling Quick Actions

func application(application: UIApplication,

performActionForShortcutItem shortcutItem: UIApplicationShortcutItem,

completionHandler: Bool -> Void) {

let didHandle: Bool = /* handle the quick action using shortcutItem */

completionHandler(didHandle)

}

On app activationHandling Quick Actions

func application(application: UIApplication,

performActionForShortcutItem shortcutItem: UIApplicationShortcutItem,

completionHandler: Bool -> Void) {

let didHandle: Bool = /* handle the quick action using shortcutItem */

completionHandler(didHandle)

}

On app launchHandling Quick Actions

func application(application: UIApplication,

didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

}

On app launchHandling Quick Actions

func application(application: UIApplication,

didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

var performAdditionalHandling = true

if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey]

as? UIApplicationShortcutItem {

/* handle the quick action using shortcutItem */

performAdditionalHandling = false

}

return performAdditionalHandling

}

On app launchHandling Quick Actions

func application(application: UIApplication,

didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

var performAdditionalHandling = true

if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey]

as? UIApplicationShortcutItem {

/* handle the quick action using shortcutItem */

performAdditionalHandling = false

}

return performAdditionalHandling

}

On app launchHandling Quick Actions

func application(application: UIApplication,

didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

var performAdditionalHandling = true

if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey]

as? UIApplicationShortcutItem {

/* handle the quick action using shortcutItem */

performAdditionalHandling = false

}

return performAdditionalHandling

}

Best practicesHome Screen Quick Actions

Best practicesHome Screen Quick Actions

Every app should provide quick actions

Best practicesHome Screen Quick Actions

Every app should provide quick actionsFocus on providing quick access to high-value tasks

Best practicesHome Screen Quick Actions

Every app should provide quick actionsFocus on providing quick access to high-value tasksMake quick actions predictable

Best practicesHome Screen Quick Actions

Every app should provide quick actionsFocus on providing quick access to high-value tasksMake quick actions predictableBe prepared to handle dynamic quick actions from a previous version of your app

Best practicesHome Screen Quick Actions

Every app should provide quick actionsFocus on providing quick access to high-value tasksMake quick actions predictableBe prepared to handle dynamic quick actions from a previous version of your appDon’t add functionality that is only accessible using quick actions

Peter Hajas UIKit Frameworks Engineer

Peek and PopSeamlessly preview and navigate to content

Preview

Commit

Adding Peek and Pop to Your AppComponents of the interaction

Adding Peek and Pop to Your App

Registered View Controller

Components of the interaction

Adding Peek and Pop to Your App

Registered View Controller

Source

Registered View Controller

Components of the interaction

Adding Peek and Pop to Your App

Registered View Controller Previewed View Controller

Source

Registered View Controller

Source

Components of the interaction

Adding Peek and Pop to Your App

Registered View Controller Previewed View Controller

Source

Components of the interaction

Conforming to UIViewControllerPreviewingDelegateAdding Peek and Pop to Your App

class ChatTableViewController : UITableViewController,

UIViewControllerPreviewingDelegate

Conforming to UIViewControllerPreviewingDelegateAdding Peek and Pop to Your App

class ChatTableViewController : UITableViewController,

UIViewControllerPreviewingDelegate

Registering for previewingAdding Peek and Pop to Your App

override func viewDidLoad() {

super.viewDidLoad()

registerForPreviewing(with: self, sourceView: tableView)

}

Registering for previewingAdding Peek and Pop to Your App

override func viewDidLoad() {

super.viewDidLoad()

registerForPreviewing(with: self, sourceView: tableView)

}

Registering for previewingAdding Peek and Pop to Your App

override func viewDidLoad() {

super.viewDidLoad()

registerForPreviewing(with: self, sourceView: tableView)

}

Providing a preview view controllerAdding Peek and Pop to Your App

func previewingContext(_ previewingContext: UIViewControllerPreviewing,

viewControllerForLocation location: CGPoint) -> UIViewController? {

}

Providing a preview view controllerAdding Peek and Pop to Your App

func previewingContext(_ previewingContext: UIViewControllerPreviewing,

viewControllerForLocation location: CGPoint) -> UIViewController? {

}

func previewingContext(_ previewingContext: UIViewControllerPreviewing,

viewControllerForLocation location: CGPoint) -> UIViewController? {

guard let indexPath = tableView.indexPathForRow(at: location) else { return nil }

}

Providing a preview view controllerAdding Peek and Pop to Your App

func previewingContext(_ previewingContext: UIViewControllerPreviewing,

viewControllerForLocation location: CGPoint) -> UIViewController? {

guard let indexPath = tableView.indexPathForRow(at: location) else { return nil }

let chatDetailViewController = ...

chatDetailViewController.chatItem = chatItem(at: indexPath)

}

Providing a preview view controllerAdding Peek and Pop to Your App

func previewingContext(_ previewingContext: UIViewControllerPreviewing,

viewControllerForLocation location: CGPoint) -> UIViewController? {

guard let indexPath = tableView.indexPathForRow(at: location) else { return nil }

let chatDetailViewController = ...

chatDetailViewController.chatItem = chatItem(at: indexPath)

let cellRect = tableView.rectForRow(at: indexPath)

let sourceRect = previewingContext.sourceView.convert(cellRect, from: tableView)

previewingContext.sourceRect = sourceRect

}

Providing a preview view controllerAdding Peek and Pop to Your App

func previewingContext(_ previewingContext: UIViewControllerPreviewing,

viewControllerForLocation location: CGPoint) -> UIViewController? {

guard let indexPath = tableView.indexPathForRow(at: location) else { return nil }

let chatDetailViewController = ...

chatDetailViewController.chatItem = chatItem(at: indexPath)

let cellRect = tableView.rectForRow(at: indexPath)

let sourceRect = previewingContext.sourceView.convert(cellRect, from: tableView)

previewingContext.sourceRect = sourceRect

return chatDetailViewController

}

Providing a preview view controllerAdding Peek and Pop to Your App

Committing a preview view controllerAdding Peek and Pop to Your App

func previewingContext(_ previewingContext: UIViewControllerPreviewing,

commit viewControllerToCommit: UIViewController) {

show(viewControllerToCommit, sender: self)

}

Committing a preview view controllerAdding Peek and Pop to Your App

func previewingContext(_ previewingContext: UIViewControllerPreviewing,

commit viewControllerToCommit: UIViewController) {

show(viewControllerToCommit, sender: self)

}

Committing a preview view controllerAdding Peek and Pop to Your App

func previewingContext(_ previewingContext: UIViewControllerPreviewing,

commit viewControllerToCommit: UIViewController) {

show(viewControllerToCommit, sender: self)

}

Preview quick actions

Registered View Controller Previewed View Controller

Source

Peek and Pop

override func previewActionItems() -> [UIPreviewActionItem] {

let heart = UIPreviewAction(title: "❤", style: .default) { (action, viewController) in

// Send a heart

}

return [heart]

}

Preview quick actionsAdding Peek and Pop to Your App

override func previewActionItems() -> [UIPreviewActionItem] {

let heart = UIPreviewAction(title: "❤", style: .default) { (action, viewController) in

// Send a heart

}

return [heart]

}

Preview quick actionsAdding Peek and Pop to Your App

let replyActions = [UIPreviewAction(title: "❤", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😄", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "👍", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😯", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😢", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😈", style: .default, handler: replyActionHandler)]

let sendReply = UIPreviewActionGroup(title: "Send Reply…",

style: .default,

actions: replyActions)

Preview quick action groupsAdding Peek and Pop to Your App

let replyActions = [UIPreviewAction(title: "❤", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😄", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "👍", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😯", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😢", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😈", style: .default, handler: replyActionHandler)]

let sendReply = UIPreviewActionGroup(title: "Send Reply…",

style: .default,

actions: replyActions)

Preview quick action groupsAdding Peek and Pop to Your App

let replyActions = [UIPreviewAction(title: "❤", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😄", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "👍", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😯", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😢", style: .default, handler: replyActionHandler),

UIPreviewAction(title: "😈", style: .default, handler: replyActionHandler)]

let sendReply = UIPreviewActionGroup(title: "Send Reply…",

style: .default,

actions: replyActions)

Preview quick action groupsAdding Peek and Pop to Your App

Other preview quick action stylesAdding Peek and Pop to Your App

let save = UIPreviewAction(title: "Chat Saved", style: .selected, handler: saveHandler)

let block = UIPreviewAction(title: "Block", style: .destructive, handler: blockHandler)

Best practicesPeek and Pop

Best practicesPeek and Pop

Content that can be tapped should support Peek and Pop

Best practicesPeek and Pop

Content that can be tapped should support Peek and PopReturn a preview view controller consistently

Best practicesPeek and Pop

Content that can be tapped should support Peek and PopReturn a preview view controller consistentlyDon’t take too long in the previewing delegate

Best practicesPeek and Pop

Content that can be tapped should support Peek and PopReturn a preview view controller consistentlyDon’t take too long in the previewing delegateSet the previewing context sourceRect

Peek and Pop feel with your user interfaceUIPreviewInteraction

NEW

Same Peek and Pop Force Processing

Automatic Haptic Feedback Your User Interface

UIPreviewInteraction

+ +

extension ChatDetailViewController : UIPreviewInteractionDelegate

UIPreviewInteractionDelegateUIPreviewInteraction

override func viewDidLoad() {

super.viewDidLoad()

replyPreviewInteraction = UIPreviewInteraction(view: view)

replyPreviewInteraction.delegate = self

}

Creating the UIPreviewInteractionUIPreviewInteraction

override func viewDidLoad() {

super.viewDidLoad()

replyPreviewInteraction = UIPreviewInteraction(view: view)

replyPreviewInteraction.delegate = self

}

Creating the UIPreviewInteractionUIPreviewInteraction

Starting the interactionUIPreviewInteraction

previewInteractionShouldBegin()previewInteractionShouldBegin()

Preview

State transitionsUIPreviewInteraction

Preview

State transitionsUIPreviewInteraction

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

Preview

State transitionsUIPreviewInteraction

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

0.200.400.60

0.80

Preview

State transitionsUIPreviewInteraction

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

0.10

Preview

0.20

State transitionsUIPreviewInteraction

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

0.40

0.600.80

1.00Preview

func previewInteraction(_ previewInteraction: UIPreviewInteraction,

didUpdatePreviewTransition transitionProgress: CGFloat,

ended: Bool) {

updateForPreview(progress: transitionProgress)

if ended {

completePreview()

}

}

Preview transitionUIPreviewInteraction

func previewInteraction(_ previewInteraction: UIPreviewInteraction,

didUpdatePreviewTransition transitionProgress: CGFloat,

ended: Bool) {

updateForPreview(progress: transitionProgress)

if ended {

completePreview()

}

}

Preview transitionUIPreviewInteraction

func previewInteraction(_ previewInteraction: UIPreviewInteraction,

didUpdatePreviewTransition transitionProgress: CGFloat,

ended: Bool) {

updateForPreview(progress: transitionProgress)

if ended {

completePreview()

}

}

Preview transitionUIPreviewInteraction

func previewInteractionDidCancel(_ previewInteraction: UIPreviewInteraction) {

UIView.animate(withDuration: 0.4) {

self.updateForPreview(progress: 0)

self.resetToInitialAppearance()

}

}

CancellationUIPreviewInteraction

func previewInteractionDidCancel(_ previewInteraction: UIPreviewInteraction) {

UIView.animate(withDuration: 0.4) {

self.updateForPreview(progress: 0)

self.resetToInitialAppearance()

}

}

CancellationUIPreviewInteraction

State transitionsUIPreviewInteraction

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

Preview

State transitionsUIPreviewInteraction

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdateCommitTransition:ended:)

Preview

Commit

State transitionsUIPreviewInteraction

0.200.400.600.80

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdateCommitTransition:ended:)

Preview

Commit

State transitionsUIPreviewInteraction

0.10previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdateCommitTransition:ended:)

Preview

Commit

State transitionsUIPreviewInteraction

0.200.400.600.801.00

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdateCommitTransition:ended:)

Preview

Commit

State transitionsUIPreviewInteraction

0.200.400.600.801.00

0.200.400.600.80

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdateCommitTransition:ended:)

Preview

Commit

State transitionsUIPreviewInteraction

0.200.400.600.801.00

0.10

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdateCommitTransition:ended:)

Preview

Commit

State transitionsUIPreviewInteraction

0.200.400.600.801.00

0.200.400.600.801.00

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdatePreviewTransition:ended:)

previewInteraction(didUpdatePreviewTransition:ended:)previewInteraction(didUpdateCommitTransition:ended:)

Preview

Commit

func previewInteraction(_ previewInteraction: UIPreviewInteraction,

didUpdateCommitTransition transitionProgress: CGFloat,

ended: Bool) {

updateForCommit(progress: transitionProgress)

if ended {

completeCommit()

}

}

Commit transitionUIPreviewInteraction

func previewInteraction(_ previewInteraction: UIPreviewInteraction,

didUpdateCommitTransition transitionProgress: CGFloat,

ended: Bool) {

updateForCommit(progress: transitionProgress)

if ended {

completeCommit()

}

}

Commit transitionUIPreviewInteraction

func previewInteraction(_ previewInteraction: UIPreviewInteraction,

didUpdateCommitTransition transitionProgress: CGFloat,

ended: Bool) {

updateForCommit(progress: transitionProgress)

if ended {

completeCommit()

}

}

Commit transitionUIPreviewInteraction

Low-Level Force API

Low-Level Force API

Normalized access to force data

force maximumPossibleForce

Low-Level Force API

Normalized access to force dataProperties on UITouch: force and maximumPossibleForce

force maximumPossibleForce

Low-Level Force API

Normalized access to force dataProperties on UITouch: force and maximumPossibleForceAvailable on devices that support 3D Touch or Apple Pencil

force maximumPossibleForce

Low-Level Force API

Normalized access to force dataProperties on UITouch: force and maximumPossibleForceAvailable on devices that support 3D Touch or Apple Pencil

Leveraging Touch Input on iOS Pacific Heights Thursday 10:00AM

Summary

Summary

Home screen quick actions let you jump straight into action

Summary

Home screen quick actions let you jump straight into actionPeek and Pop allow you to quickly preview and navigate to content

Summary

Home screen quick actions let you jump straight into actionPeek and Pop allow you to quickly preview and navigate to contentUIPreviewInteraction opens up new possibilities to make your app more immersive

Summary

Home screen quick actions let you jump straight into actionPeek and Pop allow you to quickly preview and navigate to contentUIPreviewInteraction opens up new possibilities to make your app more immersiveUsers expect your apps to support 3D Touch

More Information

https://developer.apple.com/wwdc16/228

Related Sessions

Advances in UIKit Animations and Transitions Pacific Heights Wednesday 5:00PM

Leveraging Touch Input on iOS Pacific Heights Thursday 10:00AM

Labs

Cocoa Touch and 3D Touch Lab Frameworks Lab C Friday 10:30AM