#WWDC19•

Modernizing Your UI for iOS 13

David Duncan, iOS System Experience© 2019 Apple Inc. All rights reserved. Redistribution or public display not permitted without written permission from Apple.

•Flexible UI

•

Bars

•

Presentations•

Search•Gestures

•

Menus

•

Flexible UIDavid Duncan

Launch Storyboards

Launch Storyboards

Launch Images

Be Resizable

Be Resizable

Be ResizableActions

Adopt Launch Storyboards
Support any size
Support Split Screen Multitasking

Required April 2020•BarsNew Bar Appearance

Free when you link against iOS 13Appearance Customization

let appearance = UINavigationBarAppearance()appearance.configureWithOpaqueBackground()appearance.titleTextAttributes = [.foregroundColor: myAppLabelColor]appearance.largeTitleTextAttributes = [.foregroundColor: myAppLabelColor]navigationBar.standardAppearance = appearance

let appearance = UINavigationBarAppearance()appearance.configureWithOpaqueBackground()appearance.titleTextAttributes = [.foregroundColor: myAppLabelColor]appearance.largeTitleTextAttributes = [.foregroundColor: myAppLabelColor]navigationBar.standardAppearance = appearance

let appearance = UINavigationBarAppearance()appearance.configureWithOpaqueBackground()appearance.titleTextAttributes = [.foregroundColor: myAppLabelColor]appearance.largeTitleTextAttributes = [.foregroundColor: myAppLabelColor]navigationBar.standardAppearance = appearance

let appearance = UINavigationBarAppearance()appearance.configureWithOpaqueBackground()appearance.titleTextAttributes = [.foregroundColor: myAppLabelColor]appearance.largeTitleTextAttributes = [.foregroundColor: myAppLabelColor]navigationBar.standardAppearance = appearance

let appearance = UINavigationBarAppearance()appearance.configureWithOpaqueBackground()appearance.titleTextAttributes = [.foregroundColor: myAppLabelColor]appearance.largeTitleTextAttributes = [.foregroundColor: myAppLabelColor]navigationBar.standardAppearance = appearance

// Customizing.standardAppearance

// Customizing.compactAppearance

// Customizing.scrollEdgeAppearance

// Customizing.buttonAppearance.doneButtonAppearanceCustomizing Bars

UIToolbar: UIToolbarAppearance

UITabBar: UITabBarAppearance

// Customizing.stackedLayoutAppearance.inlineLayoutAppearance.compactInlineLayoutAppearance

let appearance = navigationBar.standardAppearance.copy()// Configure whatever else you wantnavigationItem.standardAppearance = appearance

let appearance = navigationBar.standardAppearance.copy()// Configure whatever else you wantnavigationItem.standardAppearance = appearance

let appearance = navigationBar.standardAppearance.copy()// Configure whatever else you wantnavigationItem.standardAppearance = appearance

let appearance = navigationBar.standardAppearance.copy()// Configure whatever else you wantnavigationItem.standardAppearance = appearance•

Presentations

Russell LaddSheetsSheetUIModalPresentationStyle.pageSheetUIModalPresentationStyle.pageSheet.formSheetReadable WidthReadable WidthReadable WidthUIModalPresentationStyle.pageSheet.formSheetNEW

UIModalPresentationStyle.automatic.pageSheet.formSheetclass MyViewController: UIViewController {func pickPhoto() {let imagePicker = UIImagePickerController()imagePicker.sourceType = .photoLibrarypresent(imagePicker, animated: true)}
func presentCamera() {let camera = UIImagePickerController()camera.sourceType = .camera
present(camera, animated: true)}}class MyViewController: UIViewController {func pickPhoto() {let imagePicker = UIImagePickerController()imagePicker.sourceType = .photoLibrarypresent(imagePicker, animated: true)}
func presentCamera() {let camera = UIImagePickerController()camera.sourceType = .camera
present(camera, animated: true)}}class MyViewController: UIViewController {func pickPhoto() {let imagePicker = UIImagePickerController()imagePicker.sourceType = .photoLibrarypresent(imagePicker, animated: true)}
func presentCamera() {let camera = UIImagePickerController()camera.sourceType = .camera
present(camera, animated: true)}}class MyViewController: UIViewController {afunc showOptions() {alet optionsVC = MyOptionsViewController()present(optionsVC, animated: true)}afunc showCustomCamera() {alet cameraVC = MyCameraViewController()present(cameraVC, animated: true)}a}aclass MyViewController: UIViewController {afunc showOptions() {alet optionsVC = MyOptionsViewController()present(optionsVC, animated: true)}afunc showCustomCamera() {alet cameraVC = MyCameraViewController()present(cameraVC, animated: true)}a}aclass MyViewController: UIViewController {afunc showOptions() {alet optionsVC = MyOptionsViewController()present(optionsVC, animated: true)}afunc showCustomCamera() {alet cameraVC = MyCameraViewController()present(cameraVC, animated: true)}a}aclass MyViewController: UIViewController {afunc showOptions() {alet optionsVC = MyOptionsViewController()present(optionsVC, animated: true)}afunc showCustomCamera() {alet cameraVC = MyCameraViewController()cameraVC.modalPresentationStyle = .fullScreenpresent(cameraVC, animated: true)}a}aclass MyViewController: UIViewController {func showOptions() {let optionsVC = MyOptionsViewController()
optionsVC.modalPresentationStyle = .popoverpresent(optionsVC, animated: true)}}class MyViewController: UIViewController {func showOptions() {let optionsVC = MyOptionsViewController()
optionsVC.modalPresentationStyle = .popoverpresent(optionsVC, animated: true)}}NEWclass EmailController: UIViewController, UIAdaptivePresentationControllerDelegate {func draftDidChange() {isModalInPresentation = draft.hasChanges}
func presentationControllerDidAttemptToDismiss(_: UIPresentationController) {// Present action sheet}}NEWclass EmailController: UIViewController, UIAdaptivePresentationControllerDelegate {func draftDidChange() {isModalInPresentation = draft.hasChanges}
func presentationControllerDidAttemptToDismiss(_: UIPresentationController) {// Present action sheet}}NEWclass EmailController: UIViewController, UIAdaptivePresentationControllerDelegate {func draftDidChange() {isModalInPresentation = draft.hasChanges}
func presentationControllerDidAttemptToDismiss(_: UIPresentationController) {// Present action sheet}}User Begins PullingpresentedViewController.isModalInPresentation

User Releases with Intent

True
presentationControllerDidAttemptToDismissUser Begins PullingpresentedViewController.isModalInPresentation

User Releases with Intent

True
presentationControllerDidAttemptToDismiss

False

presentationControllerShouldDismiss

False

True

presentationControllerWillDismiss

presentationControllerDidDismissUser Begins PullingpresentedViewController.isModalInPresentation

User Releases with Intent

True
presentationControllerDidAttemptToDismiss

False

presentationControllerShouldDismiss

False

TruepresentationControllerWillDismiss

presentationControllerDidDismissShare Extensions,,,

Principal View Controller• Set isModalInPresentation• Respond topresentationControllerDidAttemptToDismissShare Extensions,,,

Principal View Controller• Set isModalInPresentation• Respond topresentationControllerDidAttemptToDismissShare Extensions,,,

Principal View Controller• Set isModalInPresentation• Respond topresentationControllerDidAttemptToDismissShare Extensions,,,

Principal View Controller• Set isModalInPresentation• Respond topresentationControllerDidAttemptToDismissAppearance Callbacks

Full Screen

PresentationDismissalpresentingViewController

viewWillDisappear

viewDidDisappear

viewWillAppear

viewDidAppear

presentedViewController

viewWillAppear

viewDidAppear

viewWillDisappear

viewDidDisappear

Appearance Callbacks

Page and Form Sheet

Presentation

Dismissal

presentingViewController

viewWillDisappear

viewDidDisappear

viewWillAppear

viewDidAppear

presentedViewController

viewWillAppear

viewDidAppear

viewWillDisappear

viewDidDisappear

UIWindow
rootViewController.view

UIWindow
Private UIKit View(s)rootViewController.view

Updating Your App

Use sheets
Implement the modal flow•Search

Kyle Sluder

UISearchController

UISearchBar

Cancel Button
Scope BarUISearchBar

Cancel Button
Scope BarNEWNEWUISearchTextField

NEW

UISearchTextField

NEWfunc loadView() {let searchController = UISearchController(searchResultsController: /*...*/)// Don’t automatically show the cancel button or scope bar
searchController.automaticallyShowsCancelButton = false
searchController.automaticallyShowsScopeBar = false// Customize appearance of the search text field
let searchField = searchController.searchBar.textField
searchField.textColor = UIColor(named: "MyPinkColor")
searchField.font = UIFont(name: "American Typewriter", size: 18)/* ... */}func loadView() {let searchController = UISearchController(searchResultsController: /*...*/)// Don’t automatically show the cancel button or scope bar
searchController.automaticallyShowsCancelButton = false
searchController.automaticallyShowsScopeBar = false// Customize appearance of the search text field
let searchField = searchController.searchBar.textField
searchField.textColor = UIColor(named: "MyPinkColor")
searchField.font = UIFont(name: "American Typewriter", size: 18)/* ... */}func loadView() {let searchController = UISearchController(searchResultsController: /*...*/)// Don’t automatically show the cancel button or scope bar
searchController.automaticallyShowsCancelButton = false
searchController.automaticallyShowsScopeBar = false// Customize appearance of the search text field
let searchField = searchController.searchBar.textField
searchField.textColor = UIColor(named: "MyPinkColor")
searchField.font = UIFont(name: "American Typewriter", size: 18)/* ... */}Search Results ControllerNEWSearch Results Controller

func loadView() {let searchController = UISearchController(searchResultsController: /*...*/)// Show the search results controller as soon as search is activatedsearchController.showsSearchResultsController = true
self.navigationItem.searchController = searchController/* ... */}func loadView() {let searchController = UISearchController(searchResultsController: /*...*/)// Show the search results controller as soon as search is activatedsearchController.showsSearchResultsController = true
self.navigationItem.searchController = searchController/* ... */}Search Tokens

Available on any UISearchTextField
Copy, Paste
Drag and DropNEW

Search Tokens

Available on any UISearchTextField
Copy, Paste
Drag and Drop

NEWTokens and Text

Tokens always precede text
Can be selected and deleted
Mixed selections allowedTokens and Text

Tokens always precede text
Can be selected and deleted
Mixed selections allowedTokens and Text

Tokens always precede text
Can be selected and deleted
Mixed selections allowedTokens and Text

Tokens always precede text
Can be selected and deleted
Mixed selections allowedCreating Tokens

Creating Tokens

let selectedText = field.textIn(field.selectedTextRange) // "beach"
let token = UISearchToken(icon: nil, text: selectedText)
field.replaceTextualPortion(of: field.selectedTextRange, with: token, at: field.tokens.count)Creating Tokens.selectedTextRange

let selectedText = field.textIn(field.selectedTextRange) // "beach"
let token = UISearchToken(icon: nil, text: selectedText)
field.replaceTextualPortion(of: field.selectedTextRange, with: token, at: field.tokens.count)Creating Tokens

let selectedText = field.textIn(field.selectedTextRange) // "beach"
let token = UISearchToken(icon: nil, text: selectedText)
field.replaceTextualPortion(of: field.selectedTextRange, with: token, at: field.tokens.count)Creating Tokens

let selectedText = field.textIn(field.selectedTextRange) // "beach"
let token = UISearchToken(icon: nil, text: selectedText)
field.replaceTextualPortion(of: field.selectedTextRange, with: token, at: field.tokens.count)Creating Tokens

let selectedText = field.textIn(field.selectedTextRange) // "beach"
let token = UISearchToken(icon: nil, text: selectedText)
field.replaceTextualPortion(of: field.selectedTextRange, with: token, at: field.tokens.count)Creating Tokens

let selectedText = field.textIn(field.selectedTextRange) // "beach"
let token = UISearchToken(icon: nil, text: selectedText)
field.replaceTextualPortion(of: field.selectedTextRange, with: token, at: field.tokens.count)Ranges and Positions

beach

Ranges and Positions

UITextPosition

beach

Ranges and Positions

UITextPosition

beach

Ranges and Positions.beginningOfDocument

beach.endOfDocument

Ranges and Positions.beginningOfDocument
beach.endOfDocument

Ranges and Positions.beginningOfDocument
beach.endOfDocument

Ranges and Positions.textualRange

beach

Ranges and Positions

textualRange.startbeach
textualRange.end

Updating Your App

Take control with UISearchController
Customize appearance using UISearchTextField
Adopt UISearchToken
Check out the sample project•Gestures

James Magahern•

Selection gestures in custom text views•

Multiple selection gestures in tables and collections•

Editing gestures

UITextInteraction

Three lines of code
Editable and non-editable text interactions
Use UITextInput protocol to control selection UINEW

// Adding UITextInteractions to Your App// Create selection interaction with type .editable or nonEditable
let selectionInteraction = UITextInteraction(for: .editable)// Assign `textInput` property to your view that implements the UITextInput protocol
selectionInteraction.textInput = textView// Add the interaction to the view
textView.addInteraction(selectionInteraction)•

Selection gestures in custom text views•

Multiple selection gestures in tables and collections•

Editing gesturesshift

Multiple Selection

Your app might need to adapt• "Select" turns into "Cancel" or "Done"• Show action buttons• Disable UIAdopting Multiple Selection Gestures

Only two delegate methods to implement
UITableView and UICollectionView

NEWAdopting Multiple Selection Gestures

Only two delegate methods to implement
UITableView and UICollectionView

optional func tableView(_ tableView: UITableView,
shouldBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath) -> Bool
optional func tableView(_ tableView: UITableView,
didBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath)NEWAdopting Multiple Selection Gestures

Only two delegate methods to implement
UITableView and UICollectionView

optional func tableView(_ tableView: UITableView,
shouldBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath) -> Bool
optional func tableView(_ tableView: UITableView,
didBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath)NEWAdopting Multiple Selection Gestures

Only two delegate methods to implement
UITableView and UICollectionView

optional func tableView(_ tableView: UITableView,
shouldBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath) -> Bool
optional func tableView(_ tableView: UITableView,
didBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath)NEW•

Selection gestures in custom text views•Multiple selection gestures in tables and collections•Editing gesturesEditing Gestures in iOS 13

Frees your app from adding additional UI
Unified across the entire system
Great for drawing apps
Completely free if you use UndoManager

NEW// Working With or Without Productivity Gestures
public protocol UIResponder {public var editingInteractionConfiguration: UIEditingInteractionConfiguration}
public enum UIEditingInteractionConfiguration {case `default` // System behavior, default
case none}// DisableUpdating Your App

System selection gestures in your custom text view
Boost productivity with multiple selection gestures
Stop shaking your iPad•Menus

Mohammed Jisrawi•UIContextMenuInteraction

•

UIContextMenuInteraction

Rich previews
Complex hierarchies
Nested sub-menus
In-line sections

•

UIContextMenuInteraction

Rich previews
Complex hierarchies
Nested sub-menus
In-line sections•

Consistent Gestures

3D Touch
Haptic TouchLong press
Secondary click•Consistent Gestures

3D Touch
Haptic TouchLong press
Secondary click•UIMenu and UIAction•UIMenu and UIAction

Hierarchical menu construction system
UIMenus are composable•

UIMenu and UIActionUIMenu
UIAction(“Share”)
UIAction(“Delete”)

•

UIMenu and UIAction
UIMenuUIAction(“Share”)
UIMenu(“Edit…”)
UIAction(“Copy”)
UIAction(“Duplicate”)

UIAction(“Delete”)

// Create a UIContextMenuInteraction with Some Delegate
let interaction = UIContextMenuInteraction(delegate: self)// Attach It to Our View
menuSourceView.addInteraction(interaction)

•

UIContextMenuInteractionDelegate
func contextMenuInteraction(_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration?

One required method
Return a UIContextMenuConfiguration to start
Return nil to prevent interaction

•UIContextMenuConfiguration
class UIContextMenuConfiguration: NSObject {// ...
convenience init(identifier: NSCopying?,
previewProvider: UIContextMenuContentPreviewProvider?,actionProvider: UIContextMenuActionProvider?)}let actionProvider = (suggestedActions: [UIMenuElement]) -> UIMenu? {let editMenu = UIMenu(title: "Edit…", children: [UIAction(title: "Copy") { ... },UIAction(title: "Duplicate") { ... }])return UIMenu(children: [UIAction(title: "Share") { ... },editMenu,
UIAction(title: "Delete", style: .destructive) { ... }])}.
return UIContextMenuConfiguration(identifier: "unique-ID" as NSCopying,previewProvider: nil,actionProvider: actionProvider)let actionProvider = (suggestedActions: [UIMenuElement]) -> UIMenu? {let editMenu = UIMenu(title: "Edit…", children: [UIAction(title: "Copy") { ... },UIAction(title: "Duplicate") { ... }])return UIMenu(children: [UIAction(title: "Share") { ... },editMenu,
UIAction(title: "Delete", style: .destructive) { ... }])}.
return UIContextMenuConfiguration(identifier: "unique-ID" as NSCopying,previewProvider: nil,actionProvider: actionProvider)let actionProvider = (suggestedActions: [UIMenuElement]) -> UIMenu? {let editMenu = UIMenu(title: "Edit…", children: [UIAction(title: "Copy") { ... },UIAction(title: "Duplicate") { ... }])return UIMenu(children: [UIAction(title: "Share") { ... },editMenu,
UIAction(title: "Delete", style: .destructive) { ... }])}.
return UIContextMenuConfiguration(identifier: "unique-ID" as NSCopying,previewProvider: nil,actionProvider: actionProvider)let actionProvider = (suggestedActions: [UIMenuElement]) -> UIMenu? {let editMenu = UIMenu(title: "Edit…", children: [UIAction(title: "Copy") { ... },UIAction(title: "Duplicate") { ... }])return UIMenu(children: [UIAction(title: "Share") { ... },editMenu,UIAction(title: "Delete", style: .destructive) { ... }])}.
return UIContextMenuConfiguration(identifier: "unique-ID" as NSCopying,previewProvider: nil,actionProvider: actionProvider)•Above and Beyond

Animation customization
Set source and destination
UITargetedPreview and UITargetedDragPreview

•Above and Beyond

Animation customization
Set source and destination
UITargetedPreview and UITargetedDragPreview

•

UITableView and UICollectionView// UITableViewDelegate
optional func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAtIndexPath
indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?

•UITableView and UICollectionView// UITableViewDelegate
optional func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAtIndexPath
indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?

Polished default animations
Custom presentation and dismiss

Peek and.Pop

UIViewControllerPreviewing

Peek and.Pop

UIViewControllerPreviewing

UIContextMenuInteraction

Updating Your App

Replace long-press driven menus
Adopt it alongside Drag and Drop

Ready to Launch

Make your app flexible
Give your app a modern look
Build a great search experience
Make your app powerfulMore Information
developer.apple.com/wwdc19/224