SA1042 — Оптимизация под iPad
Содержание страницы
Несмотря на то, что приложения, которые вы написали до сих пор, будут отлично работать на iPad, они не оптимизированы для iPad. На самом деле между iPhone и iPad нет большой разницы: они оба работают под управлением одинаковых операционных систем и имеют доступ к одним и тем же фреймворкам. Но у iPad гораздо больший экран, и в этом вся разница.Учитывая гораздо большую доступную экранную недвижимость, на iPad вы можете иметь различные элементы пользовательского интерфейса, которые лучше используют дополнительное пространство экрана. Вот тут-то и вступают в игру различия между приложением, оптимизированным для iPad, и приложением для iPhone, которое также работает на iPad.
В этой главе вы расскажете о следующем:
- Платформы развертывания: краткое объяснение того, как переключиться с универсального режима на поддержку только конкретной платформы.
- Контроллер split view: Использование контроллера split view для лучшего использования доступного пространства экрана на iPad.
- Улучшение вторичной панели: повторное использование экрана сведений из версии iPhone (с некоторыми корректировками) для отображения подробной информации на iPad.
- Классы размеров в раскадровке: использование классов размеров для настройки определенных экранов для iPad.
- Ваш собственный поповер: Создайте поповер меню, который будет отображаться на iPad.
- Отправить электронное письмо из приложения: Отправьте электронное письмо в службу поддержки из приложения, используя встроенную функцию электронной почты.
- Ландшафт на больших iPhone: правильно обрабатывайте ландшафтный режим для больших устройств iPhone, так как они действуют как мини-iPad в ландшафтном режиме.
Платформы развертывания
Все новые проекты iOS, созданные с помощью Xcode, по умолчанию поддерживают платформы iPhone и iPad. Тем не менее, вы все равно можете изменить приложение только для iPhone — или для iPad, если хотите, — после создания проекта. Вы не будете делать это для StoreSearch, но если вы хотите знать, как внести изменения в поддержку только определенной платформы, вот как вы это делаете.
BOS Перейдите на экран настроек проекта и выберите цель StoreSearch.
На вкладке Общие в разделе Информация о развертывании установлен флажок для каждой платформы. Вы можете проверить те, которые хотите включить, или снять галочки с тех, которые хотите отключить.

Как изменить поддержку устройства
Примечание: Хотя вы также можете включить поддержку Mac (через Mac Catalyst), чтобы тот же код для вашего приложения iOS также питал ваше приложение macOS, будет ли это работать для вашего конкретного приложения или нет, будет зависеть от используемых вами функций и сторонних библиотек.
BOS Хотя вы не внесете никаких изменений в вышеприведенные настройки, если вы не пробовали этого раньше, рекомендуется попробовать запустить симулятор iPad прямо сейчас. Имейте в виду, что симулятор iPad огромен, поэтому вам может потребоваться использовать опцию Window ▸ Fit Screen из меню Simulator, чтобы он поместился на вашем экране.

StoreSearch в симуляторе iPad
Это прекрасно работает, но, как я уже говорил, простое увеличение интерфейса до размера iPad не использует все дополнительное пространство, которое предлагает большой экран. Поэтому вместо этого вы будете использовать некоторые специальные функции, которые UIKit может предложить на iPad, такие как контроллеры split view и popovers.
Контроллер разделенного вида
На iPhone, за некоторыми исключениями, например, когда вы встраиваете контроллеры просмотра в другой, контроллер просмотра обычно управляет всем экраном.
На iPad, поскольку дисплей намного больше, контроллеры просмотра обычно управляют только частью экрана. Часто вам нужно комбинировать разные типы контента на одном экране.
Хорошим примером этого является контроллер split view. Он имеет две панели: меньшую панель слева — “основную” — обычно содержащую список элементов, и большую правую панель — “вторичную” — показывающую больше информации о том, что вы выбрали в основном списке. Каждая панель имеет свой собственный контроллер представления.
Если вы раньше пользовались iPad, то видели контроллер split view в действии, потому что он используется во многих стандартных приложениях, таких как Почта и Настройки.

Контроллер split view в альбомной и портретной ориентациях
Если iPad находится в альбомном режиме, контроллер split view имеет достаточно места для одновременного отображения обеих панелей. Однако в портретном режиме виден только контроллер вторичного вида, и приложение предоставляет кнопку, которая перемещает основную панель в поле зрения. Или вы можете провести пальцем по экрану, чтобы показать/скрыть его.
В этом разделе вы преобразуете приложение для использования контроллера разделенного представления. Это имеет некоторые последствия для организации пользовательского интерфейса.
Проверьте ориентацию iPad
Поскольку iPad имеет другие размеры, чем iPhone, он также будет использоваться по-разному. Пейзаж по сравнению с портретом становится гораздо более важным, потому что люди гораздо чаще используют iPad как боком, так и вертикально. Поэтому ваши приложения для iPad действительно должны одинаково поддерживать все ориентации.
Это означает, что приложение для iPad не должно заставлять пейзаж показывать совершенно другой пользовательский интерфейс, чем портрет. Итак, то, что вы сделали с iPhone — версией приложения, не будет летать на iPad-вы больше не сможете показыватьLandscapeViewController
, когда пользователь поворачивает устройство. Эта функция выходит из окна.
BOS На вкладке Информация появится элемент Поддерживаемые ориентации интерфейса (iPhone) с тремя элементами под ним и элемент Поддерживаемые ориентации интерфейса (iPad) с четырьмя элементами под ним.

Поддерживаемые ориентации устройств на вкладке Информация
iPad имеет свои собственные поддерживаемые ориентации. На iPhone вы обычно не хотите включать Upside Down, но на iPad вы это делаете. Если настройки не соответствуют приведенным выше, обязательно измените их в соответствии с скриншотом.
Затем запустите приложение на симуляторе iPad и убедитесь, что приложение всегда вращается так, чтобы строка поиска находилась сверху, независимо от того, в какую ориентацию вы поместили iPad.
Теперь давайте поместим этот контроллер split view в приложение.
Добавить контроллер разделенного представления
Добавить контроллер разделенного вида очень просто – вы просто добавляете объект контроллера разделенного вида в раскадровку. Разделенный вид виден только на iPad; на iPhone он остается скрытым.
Откройте Главную раскадровку. Если вы все еще находитесь в альбомном режиме, переключитесь обратно в портретный режим.
BOS Перетащите новый контроллер Split View на холст.
Контроллер Split View поставляется с несколькими предварительно прикрепленными сценами. Удалите три дополнительных контроллера вида, оставив только контроллер разделенного вида.
Вот окончательный результат после того, как я закончил:

Раскадровка с новым контроллером Split View
Контроллер разделенного вида имеет связь segue с двумя дочерними контроллерами вида: один для меньшей первичной панели слева и один для большей вторичной панели справа.
Очевидным кандидатом на первичную панель является the SearchViewController
, и DetailViewController
он перейдет во вторичную панель.
Управление-перетащите курсор с контроллера Split View на сцену поиска. Выберите Relationship Segue – основной контроллер представления.
Это помещает новую стрелку между разделенным видом и экраном поиска.
Управление-перетащите курсор с контроллера Split View на сцену Detail. Выберите Relationship Segue – вторичный контроллер представления.
Разделенное представление должно стать начальным контроллером представления, чтобы оно сначала загружалось раскадровкой.
BOS Возьмите стрелку, которая в данный момент указывает на сцену поиска — нажмите на стрелку, чтобы сначала выбрать ее, а затем перетащить — и перетащите ее на контроллер Split View. Вы также можете проверить параметр Is Initial View Controller в инспекторе атрибутов контроллера Split View вместо перетаскивания стрелки.
Теперь все взаимосвязано:

Первичная и вторичная панели соединены с разделенным видом
Этого должно быть достаточно, чтобы запустить приложение с разделенным представлением, хотя и с некоторыми проблемами:

Приложение в контроллере раздельного просмотра
Все равно потребуется немного усилий, чтобы все выглядело хорошо и работало хорошо, но это был первый шаг.
Если вы поиграете с приложением, то заметите, что оно по-прежнему использует логику из версии iPhone, и это не всегда работает так хорошо теперь, когда пользовательский интерфейс находится в разделенном виде. Например, нажатие кнопки «Цена» на панели «Новые сведения» приведет к сбою приложения…
В этой главе вы исправите приложение, чтобы убедиться, что оно не делает ничего смешного на iPad!
Исправьте основную панель
Основная панель отлично работает в альбомной ориентации, но в портретном режиме она не видна. Вы можете сделать так, чтобы он появился с помощью:
- Нажмите кнопку назад на панели навигации.
- Проведите пальцем от левого края экрана — попробуйте —
Проблема в том, что кнопка “Назад” — так называемая кнопка режима отображения — сбивает с толку, когда она называется «Назад». Вы хотите, чтобы вместо этого он сказал “Поиск”.
Исправить это очень просто – вам просто нужно дать контроллеру представления из основной панели заголовок.
BOS В SearchViewController.swiftдобавьте следующую строку вviewDidLoad()
:
title = NSLocalizedString("Search", comment: "split view primary button")
Конечно, вы используетеNSLocalizedString()
, потому что это текст, который появляется у пользователя. Подсказка: голландский перевод — “Zoeken”.
BOS Запустите приложение, и теперь заголовок кнопки режима отображения будет отображаться правильно как “Поиск”.
Упражнение: На iPad поворот в альбомную ориентацию больше не вызывает специального контроллера ландшафтного вида. Это хорошо, потому что мы не хотим использовать его в iPad-версии приложения, но вы ничего не изменили в коде. Можете ли вы объяснить, что мешает появлению ландшафтного вида?
Ответ: Ключ к разгадке-в SearchViewController
«swillTransition()
«. Это показывает ландшафтный вид, когда новый вертикальный размерный класс становится компактным. Но на iPad как горизонтальный, так и вертикальный размерный класс всегда одинаковы, независимо от ориентации устройства. В результате при вращении ничего не происходит.
Улучшение вторичной панели
Вторичная панель нуждается в дополнительной работе — она просто пока выглядит не очень хорошо. Кроме того, нажатие на строку в результатах поиска должно заполнить вторичную панель разделенного представления, а не вызывать всплывающее окно.
Вы используете DetailViewController
для обеих целей — всплывающую и вторичную панели. Итак, давайте дадим ему логическое значение, которое определяет, как он должен себя вести. На iPhone это будет всплывающее окно, на iPad-нет.
Всплывать или не всплывать
BOS Добавьте следующую переменную экземпляра в DetailViewController.swift:
var isPopUp = false
Заменить viewDidLoad()
следующим:
override func viewDidLoad() { super.viewDidLoad() if isPopUp { popupView.layer.cornerRadius = 10 let gestureRecognizer = UITapGestureRecognizer( target: self, action: #selector(close)) gestureRecognizer.cancelsTouchesInView = false gestureRecognizer.delegate = self view.addGestureRecognizer(gestureRecognizer) // Gradient view view.backgroundColor = UIColor.clear let dimmingView = GradientView(frame: CGRect.zero) dimmingView.frame = view.bounds view.insertSubview(dimmingView, at: 0) } else { view.backgroundColor = UIColor(patternImage: UIImage( named: "LandscapeBackground")!) popupView.isHidden = true } if searchResult != nil { updateUI() } }
С кодом распознавания жестов внутри if isPopUp
чека нажатие на фон не влияет на iPad. Аналогично и для линии, задающей цвет фона clearColor
.
else
Ветвь всегда скрывает всплывающее представление до SearchResult
тех пор, пока в табличном представлении не будет выбрано значение a. Фон получает изображение шаблона, чтобы все выглядело немного приятнее — это то же самое изображение, которое вы использовали с ландшафтным видом на iPhone.

Чтобы вторичная панель выглядела лучше
Изначально это означаетDetailViewController
, что он не показывает ничего, кроме узорчатого фона. Итак, вам нужно SearchViewController
сказатьDetailViewController
, что новый SearchResult
был выбран.
Раньше на iPhone SearchViewController
создавался новый экземпляр DetailViewController
каждый раз, когда вы нажимали на строку, но теперь на iPad ему нужно будет использовать существующий экземпляр из вторичной панели split view. Но как SearchViewController
узнать, что это за экземпляр?
Вам придется дать ему ссылку на the DetailViewController
. Хорошим местом для этого было SceneDelegate
бы место, где вы можете настроить доступ к контроллеру Split View и его дочерним представлениям.
BOS Добавьте следующие свойства в SceneDelegate.swiftвнутри класса:
`// MARK: — Properties
var splitVC: UISplitViewController {
return window!.rootViewController as! UISplitViewController
}
var searchVC: SearchViewController {
let nav = splitVC.viewControllers.first as! UINavigationController
return nav.viewControllers.first as! SearchViewController
}
var detailVC: DetailViewController {
let nav = splitVC.viewControllers.last as! UINavigationController
return nav.viewControllers.first as! DetailViewController
}`
Эти три вычисляемых свойства относятся к различным контроллерам представлений в приложении:
splitVC
: Контроллер раздельного просмотра верхнего уровня.searchVC
: Экран поиска в основной панели разделенного представления.detailVC
: Экран сведений во вторичной панели разделенного представления.
Обратите внимание, что как первичная, так и вторичная панели контроллера разделенного вида имеют встроенный навигационный контроллер, который встраивает каждый контроллер вида. Таким образом, вам нужно получить фактический контроллер представления, который вас интересует, из списка представлений каждого навигационного контроллера.
Создав свойства для этих контроллеров представлений, вы можете легко ссылаться на них, не копаясь в иерархии представлений, как это было в предыдущих приложениях.
BOS Теперь добавьте новое свойство в SearchViewController.swift, чтобы содержать ссылку наDetailViewController
:
weak var splitViewDetail: DetailViewController?
Обратите внимание, что вы создаете это свойство weak
. Он SearchViewController
не несет ответственности за поддержание DetailViewController
жизни, так как это работа контроллера split view. Это прекрасно работало бы и без weak
этого, но указание этого делает отношения более ясными.
Переменная является необязательной, потому что она будет работать nil
при запуске приложения на iPhone.
BOS Добавьте следующую строку scene(_:willConnectTo:options:)
в SceneDelegate.swift:
searchVC.splitViewDetail = detailVC
BOS Чтобы изменить то, что происходит, когда пользователь нажимает на результат поиска на iPad, замените tableView(_:didSelectRowAt:)
SearchViewController.swift на:
`func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
searchBar.resignFirstResponder()
if view.window!.rootViewController!.traitCollection
.horizontalSizeClass == .compact {
tableView.deselectRow(at: indexPath, animated: true)
performSegue(withIdentifier: «ShowDetail»,
sender: indexPath)
} else {
if case .results(let list) = search.state {
splitViewDetail?.searchResult = list[indexPath.row]
}
}
}`
На iPhone это все еще делает то же самое, что и раньше — появляется новый экран сведений, — но на iPad он назначает SearchResult
объект существующемуDetailViewController
, который живет во вторичной панели.
Примечание: Чтобы определить, работает ли приложение на iPhone, вы смотрите на класс горизонтального размера корневого контроллера представления окна, который является
UISplitViewController
. На iPhone горизонтальный размерный класс всегда компактен — ну, почти всегда, так как есть некоторые исключения, но об этом чуть позже. На iPad это всегда регулярно.Причина, по которой вы смотрите на класс размера из корневого контроллера представления, а не
SearchViewController
из него, заключается в том, что последний класс размера всегда компактен по горизонтали, даже на iPad, потому что он находится внутри основной панели разделенного представления.
Эти изменения сами по себе не обновляют содержимое меток в файле DetailViewController
. Итак, давайте сделаем так, чтобы это произошло.
Идеальное место для обновления меток-это наблюдатель свойств searchResult
переменной. В конце концов, пользовательский интерфейс должен быть обновлен сразу после того, как вы поместили новый SearchResult
объект в эту переменную.
BOS Измените объявление searchResult
in DetailViewController.swift:
var searchResult: SearchResult! { didSet { if isViewLoaded { updateUI() } } }
Вы уже видели эту закономерность несколько раз. Вы предоставляете didSet
наблюдателю возможность выполнять определенные функции при изменении значения свойства. После searchResult
изменения вы вызываете updateUI()
метод для установки текста на метках.
Обратите внимание, что сначала вы проверяете, загружено ли представление контроллера. Вполне возможно, что searchResult
дается объект, когда DetailViewController
он еще не загрузил свое представление — именно это и происходит в версии приложения для iPhone. В этом случае вы не хотите звонитьupdateUI()
, так как еще нет пользовательского интерфейса для обновления. isViewLoaded
Проверка гарантирует, что этот наблюдатель свойств будет использоваться только на iPad.
BOS Добавьте следующую строку в нижнюю частьupdateUI()
:
popupView.isHidden = false
Это делает вид видимым на iPad — вспомните, что viewDidLoad()
вы спрятали всплывающее окно, потому что еще ничего не было видно.
BOS Запустите приложение. Теперь вторичная панель должна отображать подробную информацию о выбранном результате поиска. Обратите внимание, что строка в таблице также остается выбранной.

Дополнительная панель отображает дополнительную информацию о выбранном элементе
Это выглядит хорошо, но есть еще несколько небольших улучшений.
Удалить фокус ввода на iPad
На iPhone имело смысл придать строке поиска фокус ввода, чтобы клавиатура появилась сразу после запуска приложения. На iPad это выглядит не так хорошо, поэтому давайте сделаем эту функцию условной.
BOS viewDidLoad()
В SearchViewController.swiftзаключите вызов becomeFirstResponder()
в условие:
if UIDevice.current.userInterfaceIdiom != .pad { searchBar.becomeFirstResponder() }
Чтобы выяснить, работает ли приложение на iPhone или iPad, вы смотрите на текущий userInterfaceIdiom
момент . Это либо .pad
или .phone
— iPod touch в данном случае считается телефоном.
Скрыть основную панель в портретном режиме
В портретном режиме после нажатия на результат поиска первичная панель остается видимой и закрывает примерно половину вторичной панели. Было бы лучше скрыть основную панель, когда пользователь делает выбор.
BOS Добавьте следующий метод в SearchViewController.swift:
// MARK: - Private Methods private func hidePrimaryPane() { UIView.animate( withDuration: 0.25, animations: { self.splitViewController!.preferredDisplayMode = .secondaryOnly }, completion: { _ in self.splitViewController!.preferredDisplayMode = .automatic } ) }
Каждый контроллер представления имеет встроенное splitViewController
свойство, которое неnil
является таковым, если контроллер представления в данный момент находится внутри a UISplitViewController
.
Вы можете указать разделенному виду изменить режим отображения.secondaryOnly
, чтобы скрыть основную панель. Вы делаете это в блоке анимации, поэтому основная панель исчезает с плавной анимацией.
Хитрость заключается в том, чтобы восстановить предпочтительный режим отображения .automatic
после завершения анимации. В противном случае основная панель остается скрытой даже в альбомной ориентации!
BOS Добавьте следующие строки tableView(_:didSelectRowAt:)
в else
предложение, сразу после if case .results
блока:
if splitViewController!.displayMode != .oneBesideSecondary { hidePrimaryPane() }
Этот .oneBesideSecondary
режим применяется только в альбомной ориентации, поэтому здесь говорится: “если разделенное представление не находится в альбомной ориентации, скройте основную панель при нажатии строки”.
Попробуйте это сделать. Переведите iPad в портретное положение, выполните поиск и коснитесь строки. Теперь основная панель будет соскальзывать, когда вы коснетесь строки в таблице.
Поздравляю! Вы успешно перепрофилировали всплывающее окно сведений, чтобы оно также работало как дополнительная панель контроллера разделенного представления. Возможно ли это в ваших собственных приложениях, зависит от того, насколько разными вы хотите видеть пользовательские интерфейсы версий iPhone и iPad.
Если вам повезет, вы сможете использовать одни и те же контроллеры представлений для обеих версий приложения, но часто вы можете обнаружить, что пользовательский интерфейс iPad для вашего приложения настолько отличается от интерфейса iPhone, что вам придется создавать все новые контроллеры представлений с некоторой дублированной логикой.
Форумы разработчиков Apple
Когда я впервые писал эту главу, то нигде в официальной документации не было объяснено, как скрыть основную панель
UISplitViewController
, и у меня возникли проблемы с ее правильной работой.В отчаянии я обратился к форумам разработчиков Apple и задал там свой вопрос. В течение нескольких часов я получил ответ от коллеги — разработчика, который столкнулся с той же проблемой и нашел решение- спасибо, пользователь “timac”!
Поэтому, если вы застряли, не забудьте заглянуть на форумы разработчиков Apple, чтобы найти решение: devforums.apple.com
Это было бы хорошее время для фиксации ваших изменений, так как вы внесли довольно много изменений.
Исправьте всплывающее окно Detail для iPhone
Теперь детальный вид хорошо работает в контроллере Split View на iPad. Но вы вернулись и протестировали iPhone, чтобы увидеть, повлияли ли ваши изменения на какую-либо из существующих функций на этой платформе?
Это то, что всегда важно протестировать — если вы вносите изменения специально для одной платформы, а ваше приложение поддерживает несколько платформ, протестируйте их на других платформах после внесения изменений.
Вы увидите несколько небольших проблем, если протестируете приложение сейчас на iPhone:
- При запуске приложения вместо сцены поиска отображается дополнительная панель контроллера разделенного представления.
- В окне поиска теперь отображается панель навигации.
- Всплывающее окно Detail теперь не отображается должным образом на iPhone, потому
isPopUp
что оно всегда ложное- попробуйте. - Ландшафтный вид имеет неправильное расположение кнопок, а также панель навигации, которая занимает место на экране.
Давайте исправим эти проблемы.
Показать основную панель при запуске
При использовании контроллера Split View на iPhone iOS автоматически сворачивает основную панель и отображает вторичную панель при запуске. Вы можете изменить это поведение, став делегатом контроллера Split View и указав поведение, которое он должен использовать на iPhone.
BOS Добавьте следующее расширение в SceneDelegate.swift:
extension SceneDelegate: UISplitViewControllerDelegate { func splitViewController( _ svc: UISplitViewController, topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column ) -> UISplitViewController.Column { if UIDevice.current.userInterfaceIdiom == .phone { return .primary } return proposedTopColumn } }
Вы просите контроллер Split View отобразить основную панель после сворачивания представлений, если текущим устройством является iPhone. Для всех остальных устройств отображается панель, предложенная операционной системой.
BOS Добавьте следующую строку в конец scene(_:willConnectTo:options:)
in SceneDelegate.swift to, чтобы указать, что SceneDelegate
это будет делегат контроллера Split View:
splitVC.delegate = self
Это должно сработать!
Запустите и протестируйте как на iPhone, так и на iPad, чтобы убедиться, что изменение работает именно так, как ожидалось, и что оно больше ничего не сломало.
Удалить панель навигации на iPhone
Мы не хотим, чтобы панель навигации отображалась, когда приложение работает на iPhone.
BOS Добавьте следующий код в SearchViewController.swift:
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if UIDevice.current.userInterfaceIdiom == .phone { navigationController?.navigationBar.isHidden = true } }
Это скроет панель навигации SearchViewController
и все ее дочерние представления при работе на iPhone.
Исправьте всплывающее окно деталей
BOS prepare(for:sender:)
В SearchViewController.swiftдобавьте строку:
detailViewController.isPopUp = true
Сделайте то же самое в LandscapeViewController.swift.
BOS Убедитесь, что экран детализации работает правильно во всех ситуациях.
Исправьте ландшафтный экран
Ландшафтный экран на iPhone в настоящее время выглядит следующим образом:

Ландшафтный вид iPhone имеет некоторые проблемы
Обратите внимание, что дополнительная панель навигации, которую вы видели ранее, теперь исчезла. Это связано с исправлением, которое вы сделали на экране поиска, чтобы удалить там панель навигации. Поэтому все, что вам нужно сделать, это исправить расположение кнопок.
Упражнение: Знаете ли вы, что вызвало изменение макета кнопки после добавления контроллера разделенного вида?
Ответ: Макет кнопки рассчитывается на основе размера кнопкиLandscapeViewController
, но размер ландшафтного вида кажется неправильным на момент выполнения расчетов макета.
Однако, поскольку ландшафтный вид является полноэкранным, вы можете просто использовать размер экрана устройства вместо размера ландшафтного вида.
Замените следующие строки tileButtons(_:)
в файле LandscapeViewController.swift:
let viewWidth = scrollView.bounds.size.width let viewHeight = scrollView.bounds.size.height
Со следующим:
let viewWidth = UIScreen.main.bounds.size.width let viewHeight = UIScreen.main.bounds.size.height
Все, что вы сделали, — это изменили ширину и высоту просмотра в зависимости от ширины и высоты экрана.
BOS Запустите приложение сейчас, и теперь ландшафтный вид должен быть выложен правильно.
Размерные классы в раскадровке
Несмотря на то, что вы поместили существующее DetailViewController
во вторичную панель, приложение не использует все это дополнительное пространство на iPad эффективно. Было бы хорошо, если бы вы могли продолжать использовать ту же логику из DetailViewController
класса, но изменить макет его пользовательского интерфейса, чтобы он лучше подходил iPad.
Если вам нравится страдать, вы можете сделать if UIDevice.current.userInterfaceIdiom == .pad``viewDidLoad()
это и переместить все ярлыки программно … но есть лучший способ. Именно для этого и были изобретены размерные классы!
Напомним, что существует два возможных класса размеров: компактный и обычный, и что одно из этих значений можно присвоить горизонтальной оси (ширина), а другое-вертикальной оси (высота).
Вот опять схема:

Горизонтальные и вертикальные классы размеров
BOS Откройте Main.storyboard и используйте панель инструментов IB для переключения на iPad (9.7″) – теперь контроллеры просмотра стали больше.
Мы хотим сделать всплывающее окно Detail больше, когда приложение работает на iPad. Однако если вы внесете какие-либо изменения в раскадровку прямо сейчас, эти изменения также повлияют на дизайн приложения в режиме iPhone. К счастью, как вы уже видели ранее, есть способ вносить изменения, применимые только к определенному классу размеров.
Вы можете сказать Interface Builder, что хотите изменить макет только для обычного класса размера ширины (wR), но оставить в покое компактную ширину (wC). Теперь эти изменения повлияют только на внешний вид приложения на iPad.
Удаление элемента для определенного класса размеров
Панель сведений не нуждается в кнопке закрытия на iPad. Это не всплывающее окно, так что нет никаких причин отмахиваться от него. Давайте уберем эту кнопку из раскадровки.
BOS Нажмите кнопку Закрыть на детальной сцене. Перейдите в инспектор атрибутов и прокрутите страницу до самого низа, до установленной опции.

Установленный флажок
Этот параметр позволяет удалить представление из определенного класса размеров, оставив его видимым в других классах размеров.
BOS Нажмите крошечную кнопку + слева от установленной. Это вызовет меню. Выберите Width: Regular, Height: Regular и нажмите Добавить вариацию:

Добавление вариации для обычного, обычного размерного класса
Это добавляет новую строку со вторым установленным флажком:

Этот параметр может быть изменен в зависимости от размера класса
BOS Снимите флажок Установлен для wR hR. Теперь кнопка закрытия исчезает со сцены — если раскадровка находится в режиме iPad, конечно.
Кнопка закрытия все еще существует, но она не установлена для этого размерного класса. Вы все еще можете видеть кнопку в контуре документа, но она выделена серым цветом:

Кнопка закрытия все еще присутствует, но выделена серым цветом
BOS Используйте панель инструментов IB, чтобы вернуться к iPhone SE.
Обратите внимание, как кнопка закрытия вернулась в исходное положение. Вы только удалили его из раскадровки для iPad. Вот в чем сила размерных классов!
Запустите приложение, и вы увидите, что кнопка закрытия действительно исчезла на iPad.:

Больше нет кнопки «закрыть» в левом верхнем углу
Изменение макета раскадровки для данного размерного класса
Конечно, всплывающее окно Detail также слишком широко на iPad :] Вы исправили это для устройств iPhone, используя вариации для классов размеров ранее. Вы можете сделать то же самое, чтобы изменить размер подробного экрана на iPad.
BOS В раскадровке снова переключитесь на макет iPad (9,7 дюйма).
BOS Выберите всплывающее окно и добавьте вариации на основе размера для wR hR для левогои правого ограничений автоматической компоновки, чтобы расстояние вокруг всплывающего окна составляло 80 точек для этого изменения размера.
Вы заметите, что у вас уже есть варианты, добавленные ранее для этих значений PIN-кода из настроек iPhone, которые вы сделали ранее.
BOS Выберите вид изображения во всплывающем окне Детали и в инспекторе размеровдобавьте изменения размера для wR hR, чтобы ширина и высота изображения были равны 180.
BOS Выберите основной вид стека и установите его интервал равным 20 для класса размера wR hR.
BOS Измените ограничения pin-кода для представления стека так, чтобы расстояние между точками составляло 32 точки.
В итоге вы должны получить что-то похожее на это:

Всплывающее окно после изменений, связанных с iPad
Просто чтобы перепроверить, переключитесь обратно на iPhone SE через панель инструментов IB и убедитесь, что панель сведений восстановлена до своих первоначальных размеров. Если нет, то вы, возможно, изменили одно из исходных ограничений вместо того, чтобы вносить изменения в размерный класс iPad.
В версии панели сведений для iPad текст теперь крошечный по сравнению с всплывающим фоном. Итак, давайте изменим шрифты. Это работает точно так же: вы добавляете настройку для этого класса размеров с помощью кнопки+, а затем изменяете свойство. Вы можете настроить любой атрибут с маленьким + перед ним для разных классов размеров.
BOS Выберите метку имени. В инспекторе атрибутов нажмите + перед шрифтом, чтобы добавить новый вариант. Выберите системный жирный шрифт, размер 28.

Добавление изменения класса размера шрифта этикетки
К сожалению, это нарушит поддержку динамического типа для iPad, но учитывая, что новые шрифты по умолчанию намного больше, мы надеемся, что это не будет иметь значения. Если это действительно важно для вас, то вам нужно будет установить различные размеры шрифта с помощью кода, основанного на типе устройства – iPhone или iPad. Но это более важная тема, которую мы не можем здесь осветить.
BOS Добавьте новый вариант, чтобы изменить шрифт других меток на Системный, размер 20. Вы можете сделать это за один раз, сделав множественный выбор.
BOS Добавьте вариант расстояния между стеками сетки, установив его равным 20.
Переключитесь обратно на iPhone SE, чтобы убедиться, что все ограничения там по-прежнему верны.
BOS Запустите приложение, и у вас должно быть гораздо более детальное представление.:

iPad теперь использует другие ограничения для вторичной панели
Упражнение: В первый раз , когда вторичная панель показывает свое содержимое, оно появляется довольно резко, потому что вы просто устанавливаете
isHidden
свойствоpopupView
tofalse
, которое заставляет его появляться мгновенно. Посмотрите, сможете ли вы заставить его появиться с помощью классной анимации.
BOS Это, вероятно, хорошее время, чтобы снова попробовать приложение на iPhone. Внесенные вами изменения должны быть совместимы с версией iPhone, но разумно убедиться в этом.
Если вы удовлетворены тем, что все работает так, как должно, то зафиксируйте изменения.
Сдвиньте и разделите экран на iPad
В iOS есть очень удобная функция разделения экрана, которая позволяет запускать два приложения бок о бок. Он работает практически на всех 64-битных iPad (с некоторыми оговорками). Поскольку вы использовали классы размера для создания пользовательского интерфейса приложения, поддержка разделения экрана должна работать безупречно.
Попробуйте: запустите приложение на одном из симуляторов iPad. Проведите пальцем вверх от нижней части экрана, чтобы ваша док-станция появилась на экране. Перетащите значок приложения из док-станции на правый (или левый) край экрана iPad, и он должен включиться, давая вам два приложения, работающих бок о бок. Вы можете перетащить разделительную полосу, чтобы настроить размер, занимаемый каждым приложением. Благодаря размерным классам планировка StoreSearch будет автоматически адаптироваться к отведенному пространству.
На панели инструментов IB есть кнопка компоновки, которая становится доступной при выборе любого из режимов iPad. Это можно использовать для изменения того, как действует контроллер представления, когда он является частью такого разделенного экрана.
Ваш собственный поповер
Любой, кто когда-либо пользовался iPad раньше, без сомнения, знаком с поповерами-плавающими панелями, которые появляются при нажатии кнопки на панели навигации или панели инструментов. Это очень удобный элемент пользовательского интерфейса.
Поповер-это не что иное, как контроллер представления, представленный особым образом. В этом разделе вы создадите всплывающее окно для простого меню.
Добавить пункты меню
BOS В раскадровке сначала переключитесь обратно на iPhone SE, потому что в режиме iPad контроллеры просмотра огромны и занимают слишком много места.
BOS Перетащите новый контроллер табличного представления на холст и поместите его рядом с экраном сведений.
Измените представление таблицы на сгруппированный стиль и придайте ему статические ячейки.
Установите идентификатор раскадровки табличного представления в PopoverView в инспекторе удостоверений.
BOS Добавьте эти строки (измените стиль ячейки на базовый).:

Дизайн нового контроллера табличного представления
Это просто помещает три пункта в таблицу. Вы будете делать что-то только с первым в этой книге. Не стесняйтесь реализовать функциональность двух других самостоятельно.
Отображение в виде всплывающего окна
Чтобы отобразить контроллер представления во всплывающем окне, вам нужна кнопка, которая запускает всплывающее окно. Однако у вас нет контроллера навигации на раскадровке — контроллер навигации автоматически добавляется контроллером Split View во время выполнения.
Поэтому вам придется добавить кнопку с помощью кода.
BOS Добавьте следующий код в конец else
блока if isPopup
условия viewDidLoad
в DetailViewController.swift:
// Popover action button navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(showPopover(_:)))
Вы устанавливаете элемент кнопки правой панели на панели навигации подробного экрана на кнопку, при нажатии которой вызывается метод с именем showPopover
. Эта кнопка не будет отображаться на iPhone, потому что там всплывающее окно Detail не находится в навигационном контроллере.
BOS Добавьте новый метод в DetailViewController.swift:
@objc func showPopover(_ sender: UIBarButtonItem) { guard let popover = storyboard?.instantiateViewController( withIdentifier: "PopoverView") else { return } popover.modalPresentationStyle = .popover if let ppc = popover.popoverPresentationController { ppc.barButtonItem = sender } present(popover, animated: true, completion: nil) }
Вы просто создаете экземпляр контроллера табличного представления с меню, используя идентификатор раскадровки, который вы настроили ранее, настраиваете его стиль представления как поповер, устанавливаете кнопку для отображения поповера, а затем отображаете его.
Если вы запустите приложение и нажмете кнопку меню, оно должно выглядеть следующим образом:

Это меню слишком высокое
Установите размер всплывающего окна
Поповер на самом деле не знает, насколько велик его контроллер представления контента, поэтому он просто выбирает размер, и это просто некрасиво. Вы можете указать ему, насколько большим должен быть контроллер представления, используя свойство preferred content size.
BOS В инспекторе атрибутов контроллера табличного представленияв полях Размер содержимого введите Width: 320, Height: 204.

Изменение предпочтительной ширины и высоты всплывающего окна
Теперь размер всплывающего меню выглядит намного более подходящим:

Всплывающее меню с подходящим размером
Когда всплывающее окно видно, все остальные элементы управления на экране становятся неактивными. Пользователь должен нажать за пределами поповера, чтобы отклонить его, прежде чем он сможет снова использовать остальную часть экрана — вы можете сделать исключения из этого, установив свойство поповераpassthroughViews
.
Отправка электронной почты из приложения
Теперь давайте заставим работать пункт меню “Отправить письмо в службу поддержки”. Разрешить пользователям отправлять электронное письмо из вашего приложения довольно просто.
iOS предоставляет MFMailComposeViewController
класс, который позаботится обо всем за вас. Он позволяет пользователю ввести электронное письмо, а затем отправить его с помощью учетной записи почты, настроенной на устройстве.
Все, что вам нужно сделать, это создать MFMailComposeViewController
объект и представить его на экране.
Вопрос в том, кто будет отвечать за этот почтовый контроллер? Это не может быть поповер, потому что этот контроллер представления будет освобожден, как только поповер исчезнет.
Вместо этого вы позволите DetailViewController
обрабатывать отправку электронной почты, главным образом потому, что это экран, который в первую очередь вызывает всплывающее окно. DetailViewController
это единственный объект, который что-то знает о поповере.
Класс MenuViewController
Чтобы все работало, вы создадите новый класс с именем MenuViewController
popover, дадите ему протокол делегата и DetailViewController
реализуете эти методы делегата.
BOS Добавьте новый файл в проект, используя шаблон класса Cocoa Touch. Назовите его MenuViewController, подкласс UITableViewController.
BOS Удалите все методы источника данных из этого файла, так как они не нужны для табличного представления со статическими ячейками. Также удалите весь закомментированный шаблонный код.
В раскадровке измените класс контроллера табличного представления popover на MenuViewController.
BOS Добавьте новый протокол в MenuViewController.swift (вне класса):
protocol MenuViewControllerDelegate: AnyObject { func menuViewControllerSendEmail(_ controller: MenuViewController) }
BOS Также добавьте свойство для этого протокола внутри класса.:
weak var delegate: MenuViewControllerDelegate?
Как и все свойства делегата, это слабое свойство, потому что вы не хотите MenuViewController
“владеть” объектом, реализующим методы делегата.
Наконец, добавьте tableView(_:didSelectRowAt:)
для обработки нажатия на строки из табличного представления.:
// MARK: - Table View Delegates override func tableView( _ tableView: UITableView, didSelectRowAt indexPath: IndexPath ) { tableView.deselectRow(at: indexPath, animated: true) if indexPath.row == 0 { delegate?.menuViewControllerSendEmail(self) } }
Установите делегат MenuViewController
Теперь вы должны сделать DetailViewController
делегат для этого меню popover.
BOS Переключитесь на DetailViewController.swift и добавьте следующее расширение в нижнюю часть исходного файла, чтобы соответствовать новому протоколу:
extension DetailViewController: MenuViewControllerDelegate { func menuViewControllerSendEmail(_: MenuViewController) { } }
В настоящее время код представляет собой просто заглушку. Через некоторое время вы заполните код реализации.
Затем измените следующим образом:showPopover(_:)
:
@objc func showPopover(_ sender: UIBarButtonItem) { guard let popover = storyboard?.instantiateViewController( withIdentifier: "PopoverView") as? MenuViewController // Change this else { return } popover.modalPresentationStyle = .popover if let ppc = popover.popoverPresentationController { ppc.barButtonItem = sender } popover.delegate = self // Add this present(popover, animated: true, completion: nil) }
Вы делаете два изменения:
- Вы приводите поповер к типу
MenuViewController
- Вы сообщаете экземпляру popover, который имеет тип
MenuController
, кто является его делегатом.
Запустите приложение и нажмите Отправить письмо в службу поддержки. Обратите внимание, что всплывающее окно еще не исчезло. Вам придется вручную отклонить его, прежде чем вы сможете показать лист составления почты.
Показать представление компоновки почты
BOS MFMailComposeViewController
Жизнь в MessageUI
фреймворке — импортируйте это в DetailViewController.swift:
import MessageUI
BOS Затем добавьте следующий код в menuViewControllerSendEmail()
(в расширении в конце)::
dismiss(animated: true) { if MFMailComposeViewController.canSendMail() { let controller = MFMailComposeViewController() controller.setSubject( NSLocalizedString("Support Request", comment: "Email subject")) controller.setToRecipients(["your@email-address-here.com"]) self.present(controller, animated: true, completion: nil) } }
Сначала код вызывает dismiss(animated:)
скрытие всплывающего окна. Этот метод требует completion
закрытия, которое до сих пор вы всегда оставляли nil
. Здесь вы реализуете закрытие — используя завершающий синтаксис — чтобы вызвать всплывающее окно MFMailComposeViewController
после того, как поповер исчезнет.
Не стоит представлять новый контроллер представления, пока предыдущий все еще находится в процессе увольнения. Вот почему вы ждете, чтобы показать лист составления почты, пока поповер не будет анимирован.
Чтобы использовать MFMailComposeViewController
объект, вы должны указать ему тему электронного письма и адрес электронной почты получателя. Вероятно, вам следует указать там свой собственный адрес электронной почты!
BOS Запустите приложение и выберите пункт меню «Отправить письмо в службу поддержки». Стандартный лист составления электронной почты должен скользить вверх-если вы находитесь на устройстве. Это вообще не будет работать на Симуляторе, извините.

Интерфейс электронной почты
Примечание: Если вы запускаете приложение на устройстве и не видите лист электронной почты, возможно, вы не создали никаких учетных записей электронной почты на своем устройстве-так что сделайте это в первую очередь.
Делегат представления компоновки почты
Обратите внимание, что кнопки «Отправить» и «Отмена» на самом деле ничего не делают. Это потому, что вам все еще нужно реализовать делегат для представления mail composer.
BOS Добавьте новое расширение в DetailViewController.swift:
extension DetailViewController: MFMailComposeViewControllerDelegate { func mailComposeController( _ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error? ) { dismiss(animated: true, completion: nil) } }
result
Параметр указывает, было ли письмо успешно отправлено или нет. Это приложение на самом деле не заботится об этом, но вы можете показать предупреждение в случае ошибки, если захотите. Проверьте документацию на наличие возможных кодов результатов.
BOS В menuViewControllerSendEmail()
методе добавьте следующую строку (после controller
создания, конечно)::
controller.mailComposeDelegate = self
Теперь, если вы нажмете кнопку Отмена или Отправить, лист составления почты будет отклонен.
Пейзаж на больших айфонах
Айфоны с большими экранами, такие как Plus, Xr, 11, 11 Pro Max, — странные звери. В основном они работают как любой другой iPhone, но иногда у них появляются идеи и они притворяются iPad.
BOS Запустите приложение на симуляторе iPhone 8 Plus, выполните поиск и поверните его в альбомную ориентацию.
Сначала вы получите пустую вторичную панель с панелью навигации. Нажмите кнопку “Поиск”, и приложение будет выглядеть примерно так:

Ландшафтный вид для больших айфонов испорчен
Приложение пытается сделать и то, и другое: показать контроллер split view и специальный ландшафтный вид одновременно. Очевидно, что это не сработает.
Эти устройства настолько велики, что кажутся почти маленькими айпадами. Дизайнеры Apple решили, что в альбомной ориентации эти телефоны должны вести себя как iPad, а потому показывать контроллер split view.
В чем тут фокус? Размерные классы, конечно! На ландшафтном уровне для этих устройств горизонтальный размерный класс является обычным, а не компактным. Но вертикальный размер-класс по-прежнему компактен, как и на меньших моделях iPhone.
Правильно показывать разделенный вид для больших айфонов
Чтобы остановить LandscapeViewController
их появление, вы должны сделать логику вращения более умной.
BOS В SearchViewController.swiftизмените willTransition(to:with:)
значение на:
override func willTransition( to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator ) { super.willTransition(to: newCollection, with: coordinator) switch newCollection.verticalSizeClass { case .compact: if newCollection.horizontalSizeClass == .compact { // Add this showLandscape(with: coordinator) } // Add this case .regular, .unspecified: hideLandscape(with: coordinator) @unknown default: break } }
Метод почти такой же, как и раньше – вы просто добавили дополнительное if
условие, чтобы проверить, что такое горизонтальный класс размера, когда есть вертикальный класс размера .comapct
. Таким образом, вы можете определить большие айфоны, которые ведут себя по-разному.
Попробуйте это сделать. Теперь iPhone Plus показывает правильный разделенный вид:

Приложение на iPhone 8 Plus с разделенным видом
Изменение режима отображения split view для iPhone
Но … есть еще одна проблема – когда приложение запускается, вы по-прежнему видите только вторичную панель. Вы должны нажать кнопку “Поиск”, чтобы увидеть основную панель. Это не очень хороший дизайн, даже если это касается только некоторых айфонов …
Исправить это легко – вам просто нужно установить другой режим отображения для разделенного вида, когда он используется на iPhone. Режим отображения определяет, как разделенное представление отображает свои дочерние контроллеры представления. В iOS 14 разделенное представление может иметь основную панель, дополнительную панель и дополнительную панель. Таким образом, потенциально в игре могут быть три панели.
То, как эти панели отображаются на данной платформе или в определенных условиях, определяется режимом отображения разделенного вида. Вы не можете напрямую изменить режим отображения для разделенного представления. Вместо этого вы устанавливаете параметрpreferredDisplayMode
, и разделенное представление пытается отобразить панели в соответствии с вашими предпочтениями, но если оно не может этого сделать – например, из – за нехватки места, — оно будет отображать то, что считает лучшим макетом.
Итак, чтобы исправить то, что происходит на больших айфонах, вам нужно указать, что вы хотите, чтобы разделенное представление отображало один дополнительный столбец помимо дополнительной панели. Мы делаем это в SceneDelegate
том случае, если с тех пор мы будем инициализировать split view до того, как он появится на экране.
BOS Добавьте следующее в конец scene(_:willConnectTo:options:)
in SceneDelegate.swift, где вы помещаете другой код, связанный с разделенным представлением:
if UIDevice.current.userInterfaceIdiom == .phone { splitVC.preferredDisplayMode = .oneBesideSecondary }
Если вам интересно, как это повлияет только на большие айфоны, а не на все айфоны, подумайте хорошенько :] Но помните, что .oneBesideSecondary
это ваш предпочтительный режим отображения. Это не обязательно означает, что именно это вы на самом деле увидите на устройстве. Если вы протестируете его, то увидите, что это не приводит к появлению двух панелей на меньшем экране iPhone.
На самом деле, даже на большом экране iPhone вы получаете совсем не то, что просили. Вы не получаете первичную панель бок о бок со вторичной панелью-вместо этого вы получаете первичную панель, накладывающуюся на вторичную панель:

Основная панель накладывается на вторичную на iPhone 8 Plus с разделенным видом
Существует отдельная настройка режима отображения, если вы действительно хотите, чтобы основная панель накладывалась на вторичную панель – .oneOverlaySecondary
вместо .oneBesideSecondary
– но iOS решила использовать стиль наложения вместо того, который мы запросили, из-за свободного места.
Это, по крайней мере, работает по назначению. Так что мы примем это – не то чтобы у нас был большой выбор :]
Добавить изменения пользовательского интерфейса на основе размерного класса для больших айфонов
Конечно, панель сведений теперь использует дизайн размером с iPhone, а не с iPad.
Это потому, что размерный класс для DetailViewController
теперь обычная ширина, компактная высота. Вы не создали конкретный дизайн для этого размерного класса, поэтому приложение использует дизайн по умолчанию.
Это нормально для размера подробного представления, но это означает, что кнопка закрытия снова видна.
BOS Откройте раскадровку, используйте панель инструментов IB, чтобы переключиться в режим iPhone 8 Plus и переключиться в ландшафтный режим — это поможет вам правильно определить классы размеров при добавлении исключений.
BOS Выберите кнопку Закрыть в детальной сцене. В инспекторе атрибутовдобавьте новую строку для Installed (для Width: Regular, Height: Compact) и снимите с нее флажок:

Добавление вариации для размерного класса ширина обычная, высота компактная
BOS Создайте и запустите приложение и протестируйте его на различных устройствах или симуляторах — iPad, меньшем iPhone, большем iPhone и т.д. —, ориентациях и внешнем виде, чтобы убедиться, что все по-прежнему выглядит и работает правильно.
И это все для приложения StoreSearch! Поздравляю вас с тем, что вы зашли так далеко, это был долгий путь.
BOS Отпразднуйте это, зафиксировав окончательную версию исходного кода и пометив ее тегом v1.0!
Вы можете найти файлы проекта для этой главы в разделе 42-iPad в папке исходного кода.