SA0721 — Преобразование приложения iOS в macOS
Содержание страницы
Если вы проработали первые главы этой книги, то наверняка создали несколько приложений для iOS. А в предыдущей главе вы создали приложение Mac на основе документов. Но в этой главе вы создадите приложение macOS из приложения iOS. Вы будете использовать код, представления и ресурсы из проекта iOS для создания своего приложения macOS.
Подавляющее большинство учебников и примеров Swift и SwiftUI в Интернете предназначены для iOS, в основном специально для iPhone. Поэтому изучение того, как повторно использовать код в проекте iOS, чтобы создать настоящее приложение Mac, будет очень ценным навыком.
Приступая к работе
Скачайте starter project-приложение для iOS, которое вы собираетесь конвертировать. Возможно, вы уже создали это приложение в предыдущих главах, но даже если вы это сделали, пожалуйста, используйте этот стартовый проект.
Создайте и запустите приложение в симуляторе iPhone и просмотрите все параметры, чтобы увидеть, как оно работает.

Экраны приложений iOS
Версия iOS использует очень распространенный навигационный шаблон, когда начальный экран предлагает выбор вариантов . Каждый выбор использует a NavigationLink
для отображения других представлений. Эти вторичные представления иногда имеют еще больше опций, которые могут быть полными навигационными представлениями, листами или диалоговыми окнами.
Для версии Mac, где вы можете использовать гораздо более широкие экраны, навигация будет отображаться на боковой панели слева. В основной части окна справа будут отображаться различные виды в зависимости от выбора навигации.
Когда вы будете работать над этой главой, будет много редактирования, которое может быть трудно объяснить и еще труднее следовать, но если вы заблудитесь, загрузите окончательный проект и проверьте код там.
Настройка приложения Mac
В Xcode создайте новый проект, используя шаблон приложения macOS и выбрав SwiftUI для интерфейса и Swift для языка. Вызовите приложение MountainAirportMac и сохраните его.
Импорт файлов кода
Чтобы начать, переключитесь на Finder и откройте папку MountainAirport внутри папки starter project. Затем выберите следующие папки и файлы и перетащите их в навигатор проектов для нового проекта Mac. Обязательно выберите Копировать элементы, если это необходимо, и создайте группы для каждого из них. Убедитесь, что цель MountainAirportMac проверена.
- Все файлы .swift в папке MountainAirport.
- Папка AwardsView.
- Папка FlightDetails.
- Папка FlightStatusBoard.
- Папка Разное.
- Папка «Модели».
- Папка SearchFlights.
- Папка временной шкалы.
После перемещения файлов удалите MountainAirport.swift из своего проекта. Это файл для iOS, который не нужен для вашего нового приложения Mac.
К концу этого процесса ваш навигатор проекта будет выглядеть следующим образом:

Навигатор проекта после импорта
Теперь у вас есть много рабочего кода из приложения iOS в вашем приложении macOS. Вы можете предположить, что классы и структуры модели в основном работают нормально и вам не нужно ничего менять. Ваша главная задача будет состоять в том, чтобы изменить код SwiftUI, чтобы пользовательский интерфейс работал на Mac. Но вы уже сэкономили себе кучу времени и хлопот, импортировав весь этот код. Затем вы импортируете активы.
Импорт активов
Помимо файлов .swift, вы можете импортировать ресурсы, используемые приложением iOS, в первую очередь значок приложения и любые изображения, используемые в пользовательском интерфейсе приложения.
Сначала перейдите в Assets.xcassets в навигаторе проекта Xcode, а затем откройте папку Assets.xcassets в окне Finder проекта iOS. (Если вы не показываете расширения файлов, они могут отображаться как ресурсы без расширения .xcassets.) Теперь перетащите каждую папку внутри этой папки в свой список активов.

Импортируемые активы
Это добавляет все активы, но проект iOS настраивает свои активы по-другому, чем проект macOS, так что теперь у вас есть некоторые домашние дела, начиная со значка приложения.
В списке активов у вас есть AppIcon, который является частью шаблона приложения, и AppIcon-1, который вы только что импортировали. К сожалению, iOS и macOS имеют очень разные требования к размеру изображений для иконок своих приложений. Лучшее решение-взять самое большое из изображений из AppIcon-1 и использовать утилиту icon Creator, чтобы сделать все правильные размеры изображений, но сейчас вы собираетесь обмануть и выбрать легкий выход.
В AppIcon-1выберите значок внизу: App Store iOS 1024pt и нажмите Command-C, чтобы скопировать его. Перейдите в AppIcon, выберите App Store — 2x и нажмите Command-V, чтобы вставить скопированное изображение. Теперь вы можете удалить AppIcon-1, и ваше приложение Mac будет использовать импортированный значок.
Примечание: Для приложения iOS вы предоставляете квадратные значки, а iOS закругляет для вас углы. Современные значки приложений Mac имеют закругленные углы с прозрачным заполнением, которое вы должны применить. Если бы вы собирались выпустить Mac-версию приложения для iOS, вам нужно было бы изменить дизайн значка, но сейчас подойдет квадратный значок.
Удалите группу launch-assets, так как приложения Mac не имеют представления запуска.
Выберите актив восходящий самолет, нажмите на изображение и убедитесь, что вы видите инспектор атрибутов справа.
В разделе Устройства установлен флажок Универсальный, что означает, что этот образ будет работать на любом устройстве Apple. Но изображение находится в коробке 3x для iPhone и iPad с очень высоким разрешением, а изображения 3x не работают в приложении Mac. Перетащите изображение из окна 3x в окно 2x, чтобы сделать его совместимым с Mac.
Повторите этот процесс для всех изображений размером 3x, не забывая о тех, что находятся в папке award-images.

Имиджевые активы
Теперь пришло время строить!
Исправление ошибок сборки
Вы импортировали все файлы кода, импортировали ресурсы, настроили значок приложения и настроили другие образы для Mac. Теперь большая задача-заставить приложение работать.
Нажмите Command-B, чтобы создать приложение, но не паникуйте, когда появится строка ошибок. Вы должны ожидать этого при импорте кода, написанного для другой платформы.
Откройте навигатор проблем, чтобы просмотреть все проблемы. Есть много предупреждений и ошибок, но исправление ошибок исправит предупреждения, поэтому пока спрячьте предупреждения, чтобы сделать дисплей менее загроможденным.
Нажмите кнопку X справа от раздела фильтра в нижней части навигатора проблем, чтобы он стал синим:

Показывать только ошибки
Теперь вам нужно будет исправить шестнадцать ошибок. Большинство из них связано с тем, что приложение iOS использует функции, недоступные в macOS.
Замена недоступных функций
Для каждой из ошибок найдите соответствующую ошибку в навигаторе проблем. Нажмите на строку с красным крестиком, чтобы перейти к строке кода с ошибкой, а затем следуйте этим инструкциям, чтобы исправить ее. Вы можете увидеть их в другом порядке, но сопоставьте имя ошибки и имя файла с исправлениями ниже:
StackNavigationViewStyle
недоступно в macOS — WelcomeView.swift:
Для этого приложения стиль по умолчанию будет в порядке, поэтому удалите navigationViewStyle
модификатор.
Нажмите Command-B еще раз, чтобы создать приложение после этого и всех других исправлений.
StackNavigationViewStyle
недоступно в macOS — AwardsView.swift:
Предварительный просмотр завернут в a NavigationView
. Это может быть очень полезно для iOS, чтобы увидеть, как будут выглядеть представления с навигационной панелью, но это не обязательно для macOS. previews
Замените это:
static var previews: some View { AwardsView() .environmentObject(AppEnvironment()) }
navigationBarItems(trailing:)
недоступно в macOS — FlightStatusBoard.swift:
Вместо элемента панели навигации для этого переключателя вы будете использовать панель инструментов Mac. Замените navigationBarItems
модификатор этим toolbar
модификатором:
.toolbar { Toggle("Hide Past", isOn: $hidePast) }
При этом используется тот же Toggle
элемент управления, но завернутый в a toolbar
вместо in navigationBar
.
InsetGroupedListStyle
недоступно в macOS — SearchFlights.swift:
Найдите ListStyle
протокол в документации разработчика и проверьте доступные стили списка. Вы можете щелкнуть по каждому из них и проверить доступность для macOS. После того как вы просмотрели варианты, измените это на .listStyle(.inset)
.
navigationBarTitle
недоступно в macOS — SearchFlights.swift:
Эквивалент macOS navigationTitle
таков: замените строку, показывающую ошибку, на:
.navigationTitle("Search Flights")
Устранение оставшихся ошибок
Вы внесли только пять изменений, но некоторые из них вызвали множество ошибок. Нажмите Command-B, чтобы снова создать приложение, и у вас будет десять оставшихся проблем, от которых нужно избавиться.
- Не удается найти тип
UIColor
в области видимости — FlightInformation.swift:
UIColor
это цветной объект UIKit. Эквивалент в AppKit есть NSColor
, но вы не собираетесь использовать это свойство в версии Mac, поэтому удалите timelineColor
вычисляемое свойство, чтобы избавиться от этой ошибки.
- Не удается найти тип
UIColor
в области видимости — FlightMapView.swift:
На этот раз вы собираетесь заменить три варианта использования UIColor
на NSColor
с.
- Последний набор проблем возникает с FlightMapView.swift, который использует a
UIViewRepresentable
для отображения представления UIKit внутри представления SwiftUI. Точно так же , как и в случае сUIColor
, вы должны изменитьUI
типы наNS
вместо этого.
В FlightMapView.swiftнайдите каждый из них и замените их NS
эквивалентами:
- измениться
UIViewRepresentable
наNSViewRepresentable
. - измениться
makeUIView
наmakeNSView
. - измениться
updateUIView
наupdateNSView
. - изменение
UIEdgeInsets
наNSEdgeInsets
Нажмите Command-B еще раз, и на этот раз приложение строится без ошибок. А если вы отключите Показывать только ошибки, то увидите, что все предупреждения тоже исчезли.
Молодец! Теперь у вас есть проект Mac app, заполненный большим количеством кода и ресурсов из приложения iOS и без каких-либо проблем со сборкой!
Прежде чем пытаться запустить приложение, перейдите в contentView.swift и замените стандарт Text("Hello, world!")
на WelcomeView()
. Теперь построй и беги.
Разверните окно так, чтобы вы могли видеть оба столбца. Вы можете видеть верхние части четырех навигационных кнопок слева и анимированную плоскость, приближающуюся к верхней части. Нажмите на верхнюю левую кнопку, и справа появятся данные. Это некрасиво, но работает! В следующих разделах вы сделаете так, чтобы он выглядел намного лучше.

Первый запуск
Стилизация боковой панели
Боковая панель в приложении будет показывать основные навигационные ссылки на другие части приложения. Откройте WelcomeView.swift и посмотрите, что он делает прямо сейчас. Основное действие находится в a NavigationView
и похоронено в том, что является сеткой NavigationLink
s. Это не та схема, которая хорошо работает на macOS, поэтому вы собираетесь заменить ее набором кнопок. Каждый из них установит переменную, которая будет диктовать, что приложение показывает в основной части окна.
Чтобы лучше соответствовать окну Mac, навигационные кнопки будут располагаться в столбце квадратных кнопок, а не в сетке высоких кнопок.
Во — первых, перейдите в WelcomeButtonView.swift и измените первый frame
— Image
модификатор-на:
.frame(width: 20, height: 20)
Измените последнееframe
, то есть измените VStack
на:
.frame(width: 155, height: 140, alignment: .leading)
Это делает кнопки короче, чтобы все они могли поместиться в одном столбце. Все навигационные кнопки на боковой панели используют этот вид кнопки.
Вернувшись в WelcomeView.swift, body
замените его следующим:
`var body: some View {
// 1
VStack {
// 2
WelcomeAnimation()
.foregroundColor(.white)
.frame(height: 40)
.padding()
// 3
Button(action: { displayState = .flightBoard }, label: {
FlightStatusButton()
})
// 4
.buttonStyle(.plain)
Button(action: { displayState = .searchFlights }, label: {
SearchFlightsButton()
}).buttonStyle(.plain)
Button(action: { displayState = .awards }, label: {
AwardsButton()
}).buttonStyle(.plain)
Button(action: { displayState = .timeline }, label: {
TimelineButton()
}).buttonStyle(.plain)
if let lastFlight = lastViewedFlight {
Button(action: {
displayState = .lastFlight
showNextFlight = true
}, label: {
LastViewedButton(name: lastFlight.flightName)
}).buttonStyle(.plain)
}
Spacer()
}
.padding()
// 5
.frame(minWidth: 190, idealWidth: 190, maxWidth: 190,
minHeight: 800, idealHeight: 800, maxHeight: .infinity)
// 6
.background(
Image(«welcome-background»)
.resizable()
.aspectRatio(contentMode: .fill)
)
}`
Хорошо, это много кода, но на самом деле меньше строк, чем было раньше. Все виды кнопок все еще там, но обернуты по-другому.
- Этот вид будет находиться внутри основного
NavigationView
, так что другой здесь не нужен. ТоZStack
,NavigationLink
с,ScrollView
иLazyVGrid
все ушли. WelcomeAnimation
это то, что показывает самолет, движущийся по вершине.- Вместо
NavigationLink
s,Button
s, задающиеdisplayState
переменную, содержат каждое из различных представлений кнопок. - Стиль кнопки установлен
.plain
так, чтобы удалить стандартный внешний вид кнопки macOS rounded rectangle и разрешить представлению устанавливать размер кнопки. VStack
Вид имеетframe
модификатор, который устанавливает минимальную, идеальную и максимальную ширину и высоту.background
Модификатор применяет изображение в качестве фона, который будет заполнять вид.
Свойства боковой панели
Сейчас вы увидите некоторые ошибки из-за body
доступа к свойствам, которые еще не существуют, поэтому прокрутите страницу до верхней части WelcomeView
структуры и добавьте следующее:
`// 1
@SceneStorage(«displayState»)
var displayState: DisplayState = .none
@SceneStorage(«lastViewedFlightID») var lastViewedFlightID: Int?
// 2
var lastViewedFlight: FlightInformation? {
if let id = lastViewedFlightID {
return flightInfo.getFlightById(id)
}
return nil
}`
А что здесь происходит?
- В предыдущих главах вы читали о
@AppStorage
том, что предоставляет оболочку свойств дляUserDefaults
.@SceneStorage
аналогично@AppStorage
, но сохраняет настройки для каждого окна, а не для всего приложения. Поскольку вы можете открыть несколько окон, показывающих разные виды, имеет смысл использовать@SceneStorage
здесь.displayState
ведет учет того, какую кнопку вы нажали, и это диктует, какое другое представление отображать.lastViewedFlightID
сохраняет опциональноInt
идентификатор рейса, который вы смотрели в последний раз. @SceneStorage
и@AppStorage
может содержать только примитивные типы, такие какString
,Int
,Double
,Bool
, или перечисления, которые соответствуют этим типам. Таким образом, вы сохраняете идентификатор последнего просмотренного рейса и используете это вычисляемое свойство, чтобы получитьFlightInformation
из него необязательный объект.
Чтобы исправить оставшиеся ошибки, добавьте это перечисление в конец файла MountainAirportMacApp.swift вне структуры:
enum DisplayState: Int { case none case flightBoard case searchFlights case awards case timeline case lastFlight }
Теперь создайте и запустите приложение, чтобы увидеть заполненную боковую панель Mac.

Боковая панель
NavigationViews в macOS
В приложении для iPhone a NavigationLink
внутри a NavigationView
выводит текущий вид наружу, а новый-внутрь, предоставляя при этом возможность вернуться назад. С приложением macOS это работает по-другому. Поскольку представления появляются бок о бок, NavigationView
в начале необходимо указать все свои представления. Эти представления могут меняться по мере изменения данных модели, но при NavigationView
первом появлении для каждой панели, которую вы хотите отобразить, должно быть соответствующее представление.
Во-первых, перейдите в contentView.swift и замените body
содержимое следующим образом::
// 1 NavigationView { // 2 WelcomeView() Text("Flight info goes here") } // 3 .navigationTitle("Mountain Airport")
Прохождение этого кода:
- Самый внешний вид-это теперь а
NavigationView
. - Внутри
NavigationView
находятся два представления, которые будут появляться бок о бок, причем одно из них пока является заполнителем. - У
NavigationView
него есть заголовок, который будет отображаться как заголовок окна.
Соберите и запустите, и вы увидите, как окно начинает собираться вместе. Вам нужно будет сделать окно шире, чтобы увидеть второй вид.

Навигационный вид
Вы можете изменить размер боковой панели, перетащив разделитель, но если вы полностью свернете ее, вы не сможете вернуть ее обратно, кроме как закрыв окно и открыв новое. Чтобы обойти эту ошибку, вы добавите в свое приложение предварительно настроенный пункт меню.
Перейдите в MountainAirportMacApp.swift и добавьте этот модификатор вWindowGroup
:
// 1 .commands { // 2 SidebarCommands() }
И что делают эти несколько строк?
commands
Модификатор-это способ добавления меню в приложение, как вы видели в предыдущей главе.SidebarCommands()
это предопределенноеCommandGroup
средство, которое добавляет пункт меню и сочетание клавиш в меню просмотра для переключения боковой панели.
Отображение представлений данных
Прямо сейчас вторая панель NavigationView отображает текстовый вид заполнителя, но в этом приложении он должен будет выбрать, что отображать, основываясь на настройкахdisplayState
:
- нет: EmptyView
- FlightBoard: FlightStatusBoard + FlightDetails
- Поисковые полеты: Поисковые полеты
- награды: AwardsView
- временная шкала: FlightTimelineView
- lastFlight: FlightDetails (для последнего просмотренного рейса)
Настройка свойств
Прежде чем вы сможете это настроить, ContentView
вам понадобятся данные для передачи в эти другие представления, поэтому добавьте эти свойства в верхнюю часть ContentView
структуры:
`// 1
@StateObject var flightInfo = FlightData()
// 2
@SceneStorage(«displayState»)
var displayState: DisplayState = .none
@SceneStorage(«lastViewedFlightID») var lastViewedFlightID: Int?
@SceneStorage(«selectedFlightID») var selectedFlightID: Int?
// 3
var selectedFlight: FlightInformation? {
if let id = selectedFlightID {
return flightInfo.getFlightById(id)
}
return nil
}
var lastViewedFlight: FlightInformation? {
if let id = lastViewedFlightID {
return flightInfo.getFlightById(id)
}
return nil
}`
И что же это все такое?
- Основная модель данных для списка рейсов в аэропорту-это та, в
flightInfo
которой вы инициализируетесь как a@StateObject
.ContentView
затем владеет этим объектом данных и может передавать его другим представлениям. - Как и в WelcomeView.swift,
@SceneStorage
содержит конкретные настройки окна.selectedFlightID
это единственный новый здесь. - Эти два вычисляемых свойства используют
@SceneStorage
свойства для получения полетной информации из основной модели.
Удалите @StateObject var flightInfo
свойство из WelcomeView.swift и замените его следующим:
var flightInfo: FlightData
Вам также необходимо отредактировать предварительный просмотр для этого:
WelcomeView(flightInfo: FlightData()) .previewLayout(.fixed(width: 190, height: 800))
Это дает предварительный просмотр некоторых данных и устанавливает его ширину и высоту в виде столбца, который будет больше похож на то, как он отображается в самом приложении.
Далее ContentView
необходимо поставить flightInfo
toWindowView
, поэтому переходите к contentView.swift и меняйте WelcomeView()
на:
WelcomeView(flightInfo: flightInfo)
Выбор вида
Теперь, когда данные готовы к использованию, замените представление Text
заполнителя в contentView.swift следующим образом::
// 1 switch displayState { case .none: // 2 EmptyView() case .flightBoard: // 3 HStack { FlightStatusBoard( flights: flightInfo.getDaysFlights(Date()) ) FlightDetails(flight: selectedFlight) } // 4 case .searchFlights: SearchFlights(flightData: flightInfo.flights) case .awards: AwardsView() case .timeline: FlightTimelineView( flights: flightInfo.flights.filter { Calendar.current.isDate( $0.localTime, inSameDayAs: Date() ) }) case .lastFlight: FlightDetails(flight: lastViewedFlight) }
Вот что делает этот код:
- Выберите вид, который будет отображаться в основной части окна, переключаясь между возможными состояниями
displayState
. - Если
displayState
установлено значение no, используйте anEmptyView
так,NavigationView
чтобы у него все еще были два представления, необходимые для определения его структуры. - На табло полета отображается ан
HStack
с двумя внутренними видами. - Другие параметры отображают соответствующие представления, как обсуждалось ранее. Параметры для этих представлений точно такие же, как и параметры, используемые
NavigationLink
s в версии iOS.
Теперь, когда вы все это добавили, Xcode показывает ошибки. Это происходит потому, что вы передаете необязательные значения FlightDetails
представлению, и оно ожидает необязательных значений. Разверните группу FlightDetails, откройте FlightDetails.swift и внесите эти изменения:
Замените два свойства вверху следующим образом::
var flight: FlightInformation? @SceneStorage("lastViewedFlightID") var lastViewedFlightID: Int?
Это устанавливает для полета необязательное значение и указывает этому представлению использовать @SceneStorage
параметр для последнего просмотренного полета.
Команда-нажмите на VStack
нее и выберите Сделать условной. Введите let flight = flight
вместо true
заполнителя.
Примечание: Иногда вы щелкаете мышью по представлению или открываете библиотеку и не видите всех ожидаемых параметров. В этом случае убедитесь, что предварительный просмотр холста открыт. Он не обязательно должен быть активным, но он должен быть открытым, чтобы показать все варианты.
Переместите onAppear
модификатор вверх прямо под строкой, которая устанавливает значение navigationTitle
так, чтобы оно находилось внутри if let
значения, и измените его действие на:
lastViewedFlightID = flight.id
что заставляет его устанавливать @SceneStorage
переменную.
И, наконец, добавьте эти два frame
модификатора кZStack
:
.frame(minWidth: 350) .frame(minHeight: 350)
Они позаботятся о том, чтобы этот вид никогда не становился слишком маленьким, чтобы отобразить все необходимое.
Вам может показаться, что было проделано много работы, чтобы зайти так далеко, но есть масса кода, к которому вы еще не прикоснулись и который просто работает.
Статус полета
Создайте и запустите приложение. Нажмите на статус полета и проверьте вкладки и переключатель Скрыть прошлое. Нажатие на рейс показывает всплывающее окно или, может быть, даже два, так что это то, что вам придется исправить.

Статус полета
Но прежде чем приступить к этому, откройте новое окно в своем приложении и нажмите там Статус полета. Вы можете выбрать разные рейсы в каждом окне и иметь разные настройки для скрытия прошлого, но когда вы меняете вкладки в одном окне, вы меняете все открытые окна.
Разверните группу FlightStatusBoard и откройте FlightStatusBoard.swift. В верхней части структуры вы увидите @AppStorage
свойство, в котором хранится выбранная вкладка.
Измените @AppStorage``@SceneStorage
значение на, чтобы сделать selectedTab
настройку окна вместо настройки приложения.
Соберите и запустите снова и протестируйте два разных окна. Теперь вы можете выбрать другую вкладку в каждом окне.
Отображение выбранного рейса
Вы уже настроили FlightDetails
представление для отображения выбранного рейса, но чтобы добавить его в список рейсов, вам необходимо изменить список, в котором отображаются все рейсы, чтобы он устанавливался selectedFlightID
при нажатии на любой рейс.
Заглянув в FlightStatusBoard.swift, вы можете увидеть, что body
он содержит aTabView
, и каждая вкладка использует FlightList
представление для отображения соответствующих данных. Таким образом, это говорит о том, что FlightList
именно это представление вам нужно отредактировать, чтобы изменить поведение списка.
Откройте FlightList.swift из группы FlightStatusBoard и добавьте его в верхнюю часть структуры:
@SceneStorage("selectedFlightID") var selectedFlightID: Int?
Это дает FlightList
доступ к selectedFlightID
тому, чтобы он мог хранить выбор для этого окна всякий раз, когда вы нажимаете на рейс.
Двигайтесь вниз по файлу, пока не увидите NavigationLink
его внутреннюю List
часть . Удалите этот NavigationLink
и два его модификатора и замените его следующим:
// 1 Button(action: { selectedFlightID = flight.id }, label: { // 2 FlightRow(flight: flight) }) // 3 .buttonStyle(.plain) // 4
Так что же здесь происходит?
- Вы заменили a
NavigationLink
на aButton
, который устанавливаетselectedFlightID
. - Содержание
Button
этого точно такое же, как и содержание самогоNavigationLink
этого . - Стиль кнопки устанавливается
.plain
равным, чтобы удалить стандартный внешний вид кнопки. - Вы удалили модификаторы
listRowBackground
иswipeActions
, которые не подходят для приложения Mac.
Прикрепите frame
модификатор к ScrollViewReader, чтобы установить минимальную ширину:
.frame(minWidth: 350)
Возможно, вы видели какую-то странную прокрутку при смене вкладок. Список рейсов прокручивается до следующего запланированного рейса, но иногда это оставляет пустые места в верхней части списка. Это происходит потомуscrollTo
, что метод устанавливает точку привязки.center
, и это не так хорошо работает в приложении Mac. Измените scrollTo
якорь на.top
, и ваш Mac будет намного лучше справляться со свитками.
Создайте и запустите приложение еще раз, а также проверьте статус полета. Нажмите на рейс, чтобы просмотреть его подробную информацию.

Выбранный рейс
Детали появляются, но что это за белая полоска? Нажмите на нее, и анимированная карта терминала появится или исчезнет. Версия iOS использует пользовательский переход для анимации кнопки, и это, кажется, работает, но кнопка не стилизована под этот дисплей.
Откройте FlightInfoPanel.swift из группы FlightDetails, и примерно на полпути вниз по коду вы увидите a Button
. Дважды щелкните открывающую скобку после слова Button, чтобы выбрать весь код кнопки, который сообщит вам, где заканчивается кнопка. После этой закрывающей скобки добавьте следующее::
.buttonStyle(.plain)
Теперь попробуйте еще раз, и кнопки будут выглядеть в самый раз. Теперь вы можете видеть анимацию кнопки, а также карту терминала. И вы не написали ни одной строки анимационного кода!
Отличная работа! Это был большой раздел, но теперь приложение действительно начинает собираться вместе.
Поиск рейсов
Первый раздел приложения теперь завершен, поэтому нажмите кнопку Поиска рейсов на боковой панели, чтобы просмотреть следующий раздел.

Поиск
Все данные есть, сегментированный выбор вверху работает, а поле поиска на панели инструментов даже позволяет выбирать из списка города. Но дисплей нуждается в работе, и нажатие на рейс приводит к сбою приложения.
Починить дисплей будет несложно. Разверните группу SearchFlights и откройте SearchResultRow.swift. Это использует a Button
для хранения представления данных, и, как вы уже делали со всеми Button
представлениями до сих пор, вам нужно установить стиль этой кнопки.
Под строкой Button
и непосредственно перед .sheet
строкой добавьте этот модификатор:
.buttonStyle(.plain)
Создайте и запустите приложение еще раз, чтобы увидеть немедленное улучшение.
Однако нажатие на рейс по-прежнему приводит к сбою приложения, и если вы посмотрите на отчет о сбое, ошибка находится в FlightSearchDetails.swift, где onAppear
находится настройка lastFlightInfo
.
Прокрутите страницу до верха этой структуры, и вы увидите, что у нее есть @EnvironmentObject
свойство. Теперь вы используете@SceneStorage
for window settings, поэтому замените @EnvironmentObject
свойство следующим образом::
@SceneStorage("lastViewedFlightID") var lastViewedFlightID: Int?
И измените onAppear
действие на это:
lastViewedFlightID = flight.id
Создайте и запустите приложение еще раз, перейдите в раздел Поиск рейсов и нажмите на любой рейс.

Результат поиска
Появляется лист с информацией о рейсе, и это здорово. Не так уж здорово, что кнопки на листах используют белый текст на белом фоне.
Нажмите на значок в правом верхнем углу листа, чтобы отклонить его и вернуться к FlightSearchDetails.swift. Ближе к концу структуры вы увидите foregroundColor
модификатор, который устанавливает цвет текста на белый. Расположение этого модификатора означает, что этот параметр применяется ко всем подвид в этом представлении, включая кнопки.
Не удаляйте его полностью, так как вы все равно хотите, чтобы текст сведений о рейсе был белым. Вырежьте модификатор из того места, где он есть, и вставьте его сразу после двух других представлений: FlightInfoPanel
в конце структуры и FlightDetailHeader
в верхней части.
Создайте и запустите приложение, а также протестируйте поисковые рейсы:

Детали поиска
Теперь выберите рейс и нажмите все доступные кнопки. On-Time History использует некоторые пользовательские рисунки и анимацию для отличного отображения инфографики, но все это просто работает, хотя это код iOS!
Но теперь, когда это FlightTimeHistory
появилось, показывая историю времени, у вас есть проблема. Как вы от него избавляетесь? На iOS вы бы провели пальцем вниз, но это не работает на macOS, так что вам придется найти другое решение.
В FlightSearchDetails.swiftнайдите кнопку «История времени«. Эта кнопка переключает вызываемое логическое showFlightHistory
значение, и эта переменная управляет отображением FlightTimeHistory
in a sheet
.
Измените этоButton
, чтобы показать поповер, вот так:
Button("On-Time History") { showFlightHistory.toggle() } .popover(isPresented: $showFlightHistory) { FlightTimeHistory(flight: flight) }
Теперь попробуйте еще раз, и вы сможете щелкнуть в любом месте за пределами представления, чтобы отклонить его:

Своевременное всплывающее окно
Если вы можете найти отмененный рейс, вы можете нажать кнопку Перебронировать рейс, которая использует стандартное системное оповещение. Кнопка Check In for Flight использует a confirmationDialog
. И теперь эта точка зрения полностью функциональна.
Стиль элементов управления в верхней части списка рейсов невелик, и лист был бы лучше с установленной рамкой, но я оставлю это как вызов для вас.
Последний просмотренный рейс
Прежде чем приступить к исправлению представления наград, обратите внимание, как появляется кнопка «Последний просмотренный рейс» после выбора рейса в разделе «Поиск рейсов».
Вы, вероятно, ожидаете длинного списка изменений, необходимых для того, чтобы это заработало, но знаете что? Вы уже сделали их все. Нажмите на него и попробуйте.
Так что это хороший короткий раздел. Переходим к наградам…
Награды посмотреть
Когда вы нажмете на свои награды, приложение выйдет из строя, сообщив, что не может их найти AppEnvironment
ObservableObject
.
В группе Models взгляните на AppEnvironment.swift, и вы увидите, что большая часть этого класса настраивает структуру данных awards. Все данные есть, но вам нужно передать их в пользовательский интерфейс awards.
Откройте AwardsView.swift из группы AwardsView и найдите AwardsView
структуру. Он ожидает, что AppEnvironment
объект будет передан ему как an @EnvironmentObject
. Но теперь, когда вы используете @SceneStorage
другие свойства , это единственное представление, к которому нужно получить доступAppEnvironment
, так почему бы не позволить ему самому владеть этими данными?
Замените @EnvironmentObject
строку на эту:
@State var flightNavigation = AppEnvironment()
Так что теперь AwardsView
у него есть своя модель данных, которую он может отображать.
Создайте и запустите приложение, а затем нажмите на свои награды. Больше никаких сбоев, но пользовательский интерфейс нуждается в работе.

Награды Пользовательский интерфейс нуждается в работе
Откройте AwardGrid.swift, чтобы увидеть структуру, которая отображает каждый раздел представления. Каждый AwardCardView
из них находится внутри а NavigationLink
, но вы собираетесь избавиться от этого. Замените все содержимое ForEach
на:
AwardCardView(award: award) .foregroundColor(.black) .aspectRatio(0.67, contentMode: .fit)
ForEach
Теперь содержит только the AwardCardView
и два его модификатора. Когда вы создаете и запускаете приложение, вы можете увидеть все награды в двух сетках, но они не кликабельны.
Примечание: Если вы видите не все изображения, убедитесь, что вы перетащили их все из поля 3x в поле 2x в Assets ▸ награда-изображения.

Награды
Еще одна проблема заключается в том, что в заголовках разделов используется белый текст. Удалите .foregroundColor(.white)
модификатор из Section
заголовка. Это позволит использовать цвет текста по умолчанию как в светлом, так и в темном режимах.
Отображение выбранной награды
Чтобы сделать награды кликабельными, откройте AwardCardView.swift, где вы собираетесь добавить модификатор листа для отображения AwardDetails
.
Во-первых, добавьте это свойство:
@State private var isPresented = false
isPresented
будет диктовать, виден ли лист или нет.
Команда-нажмите на кнопку VStack
и выберите Embed.… Это простой способ обернуть представление, убедившись, что вы получили все компоненты и отступ правильный.
Теперь замените Container
заполнитель следующим образом::
// 1 Button(action: { isPresented.toggle() }, label:
Вы увидите ошибку в конце структуры, но добавьте этот код чуть выше строки с ошибкой, чтобы она исчезла:
// 2 ) // 3 .buttonStyle(.plain) // 4 .sheet( isPresented: $isPresented, content: { AwardDetails(award: award) } )
Итак, эти два куска кода делают такие вещи:
- Создайте кнопку, которая переключает
isPresented
переменную для отображения листа. - Закройте
Button
вид, обернувVStack
его . - Установите обычный стиль кнопки, как обычно.
- Используйте a
sheet
для отображения значенияAwardDetails
для выбранной награды, еслиisPresented
оно равно true.
Пока не запускайте приложение. Есть еще одна важная особенность, которую нужно добавить к этому листу, — способ отклонить его. В iOS вы можете провести пальцем по листу вниз, чтобы избавиться от него, но, как вы уже видели, это не работает в macOS. Каждый лист должен иметь опцию «Отклонить».
Отклонение листа
Откройте AwardDetails.swift и добавьте это свойство:
@Environment(\.dismiss) var dismiss
Это дает представлению доступ к свойству среды, которое можно использовать для отклонения листа.
Затем добавьте это внутрь VStack
перед первымImage
:
// 1 HStack { Spacer() Button(action: { // 2 dismiss() }, label: { // 3 Image(systemName: "xmark.circle") .font(.largeTitle) }) // 4 .buttonStyle(.plain) }
Так что же здесь происходит?
- Вы добавляете an
HStack
с aSpacer
в качестве первого представления, чтобы подтолкнутьButton
его вправо. - Действие кнопки использует свойство
dismiss
environment для закрытия листа. - Пользовательский интерфейс кнопки представляет собой
Image
использование значка из символов SF сfont
модификатором для установки его размера. - И держу пари, ты этого не предвидел… стиль кнопки имеет значение
.plain
.
Постройте и запустите снова, и теперь вы можете просмотреть награды, нажать на награду, чтобы отобразить ее детали, и нажать крестик, чтобы закрыть лист.

Подробности о наградах
График полета
Есть еще один вид, на который нужно посмотреть. Помните, как вам пришлось изменить много UI
s на NS
s в FlightMapView.swift? Это было сделано для того, чтобы в представление временной шкалы можно было встраивать карты, используя MapKit.
Ну, знаешь что? Те изменения, которые вы внесли, — это все, что нужно этому представлению! Нажмите кнопку «Временная шкала полета», и вы увидите, как появятся карты полетов, как это было в версии iOS.

График полета показывает карту.
И это все! Ты сделал это. Теперь приложение обладает всеми функциями своего аналога iOS.
Вызов
Задача: Укладка
Панель вкладок в верхней части экрана поиска рейсов нуждается в некотором стилизации, чтобы она хорошо выглядела, а ее всплывающее окно слишком высокое. Не забудьте проверить, как все выглядит в светлом и темном режимах.
Если вам нужны некоторые подсказки, проверьте SearchFlights.swift и FlightSearchDetails.swift в папке challenge.
Ключевые моменты
- Существует много кода iOS, и вы можете использовать его в своих приложениях macOS практически без изменений.
- Приложения macOS могут иметь несколько открытых окон одновременно, поэтому вам необходимо убедиться, что ваши настройки применяются правильно. Должны ли они быть для всего приложения или для каждого окна?
- Приложения iOS имеют представления фиксированного размера, но на Mac вы должны знать о различных возможных размерах окон.
- Столкнувшись с задачей преобразования, беритесь за нее по крупицам. Сначала создайте приложение без ошибок, даже если это означает комментирование некоторых функций. Затем пройдите через интерфейс по одному разделу за раз, проверяя, что работает и что вы должны изменить.
- Вы импортировали в свое приложение 43 файла Swift, 29 из которых не требовали редактирования, и только 5 из 14 измененных файлов имели значительное количество изменений! Это сэкономило огромное количество времени и усилий.
Куда идти дальше?
Поздравляю! Ты сделал это. Вы начали с приложения iOS и повторно использовали код и ресурсы для создания приложения Mac. Вы узнали, как исправить ошибки, вызванные импортом кода iOS, и как настроить изображения для работы на Mac.
Выберите другой интересный проект iOS, возможно, один из ваших собственных проектов, один из других приложения или, возможно, что-то с открытым исходным кодом, и посмотрите, можете ли вы использовать эти методы, чтобы преобразовать его в приложение Mac.