228 A Peek at 3D Touch 03 A FINAL › videos › wwdc › 2016 › 228a...Supporting 3D Touch...
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