Как вы узнали из предыдущей главы, KMP не предоставляет фреймворк для разработки пользовательского интерфейса. Вам нужно будет использовать разные фреймворки для каждой платформы. В этой главе вы узнаете о написании пользовательского интерфейса для iOS с помощью SwiftUI. SwiftUI — это декларативный инструментарий пользовательского интерфейса, который работает на iOS, macOS, watchOS и tvOS. Это не будет подробным обсуждением SwiftUI, но оно научит вас основам.
Откройте начальный проект из этой главы. В нем есть несколько дополнительных файлов. В этой главе предполагается, что вы работаете на компьютере Mac с приложением Xcode от Apple. Если вы не используете Mac, не стесняйтесь пропустить эту главу. Если у вас нет Xcode, вы можете открыть любые файлы Swift в Android Studio.
IDE
Xcode — это IDE от Apple для разработки iOS, iPadOS, watchOS, macOS и tvOS. В этой главе вы можете редактировать свои файлы Swift либо в Xcode, либо в Android Studio. В Android Studio есть хороший редактор, но в Xcode есть возможность предварительного просмотра ваших представлений SwiftUI для вас. Выбор среды разработки зависит от вас, и в этом разделе вы познакомитесь с использованием обеих IDE.
Android -студия
Откройте начальный проект в Android Studio и выберите конфигурацию iOS. На значке может появиться красный крестик.

Выберите Редактировать конфигурации … из выпадающего меню. Ты увидишь:

Выберите телефон и цель, например iPhone 13 | iOS 15.0, и нажмите кнопку ОК.
Теперь щелкните значок молотка (или нажмите Command-F9), чтобы построить. Это создаст общую структуру, необходимую для iOS.

Xcode
Запустите Xcode и откройте каталог iosApp в стартовом проекте для этой главы. Вам не нужно выбирать xcodeproj или файл xcworkspace. Нажмите кнопку Открыть.

Как только проект будет открыт, вы увидите две папки iosApp слева:

Текущая система пользовательского интерфейса
В iOS вы обычно используете раскадровки или создаете свой пользовательский интерфейс в коде, если разрабатываете свой пользовательский интерфейс в UIKit. Под этими раскадровками находится сложный XML-файл. Несмотря на то, что редактор макетов в Xcode хорош, все равно требуется немало работы для разработки, а затем подключения к коду. SwiftUI — это декларативная система пользовательского интерфейса, полностью написанная в коде. Никаких макетов или раскадровок. Он намного проще в использовании и позволяет многократно использовать код с меньшими представлениями. Xcode предоставляет предварительные просмотры, чтобы вы могли создавать небольшие компоненты и просматривать их рядом с редактором.
Знакомство со SwiftUI
При создании проекта с помощью плагина KMM создаются два файла Swift: contentView.swift и iOSApp.swift. Эти два файла похожи на приложение “Hello World”. Они показывают текстовое поле в центре экрана со словом “Привет”. Плагин добавляет несколько файлов, чтобы упростить разработку.
Приложение
Откройте iOSApp.swift:

Отправной точкой в приложении SwiftUI является структура, которая отмечена @main
атрибутом над структурой. Эта структура обычно реализует App
протокол. App
Протокол требует, чтобы вы создали переменную с именемbody
, которая возвращает a Scene
. A Scene
— это контейнер для корневого представления иерархии представлений. A WindowGroup
— это aScene
, а также контейнер для ваших представлений. На iOS это будет содержать только одно окно, но на macOS и iPadOS оно может содержать несколько окон. Поскольку это одно выражение, возврат не требуется. Ни одно из имен этих файлов не является особенным — единственная важная часть — указать компилятору, где запускать приложение, и вы делаете это с @main
помощью тега.
Поскольку iOSApp
это не описывает, что делает ваше приложение, переименуйте файлTimezoneApp
, выбрав его на левой боковой панели и нажав Return . Введите TimezoneApp
и снова нажмите кнопку return. Затем измените struct iOSApp: App
на struct TimezoneApp: App
.
Затем добавьте следующий код, прежде var body
чем изменить цвет панели вкладок на приятный синий оттенок:
init() {
let tabBarItemAppearance = UITabBarItemAppearance()
tabBarItemAppearance.configureWithDefault(for: .stacked)
tabBarItemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.black]
tabBarItemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.white]
tabBarItemAppearance.normal.iconColor = .black
tabBarItemAppearance.selected.iconColor = .white
let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.stackedLayoutAppearance = tabBarItemAppearance
appearance.backgroundColor = .systemBlue
UITabBar.appearance().standardAppearance = appearance
if #available(iOS 15.0, *) {
UITabBar.appearance().scrollEdgeAppearance = appearance
}
}
Создайте приложение из меню продукта.

Затем запустите приложение в симуляторе iPhone.

Это должно выглядеть так:

Вы также можете запустить приложение в Android Studio. В Android Studio убедитесь, что в меню конфигурации выбрано значение iOSApp:

Затем нажмите кнопку Запуска:

Просмотр содержимого
Откройте contentView.swift. УдалитьText(“Hello”)
. Добавьте следующее в качестве первой строки в структуре:
@StateObject private var timezoneItems = TimezoneItems()
Как remember
и в Jetpack Compose (JC), StateObject
создает наблюдаемый объект, который создается один раз. Каждый раз, когда представление перерисовывается, оно будет повторно использовать существующий объект. Другие объекты могут прослушивать изменения, и SwiftUI обновит эти объекты. Если вы откроете файл TimezoneItems.swift, вы увидите, что в нем ObservableObject
публикуется список часовых поясов и выбранных часовых поясов. Он также асинхронно получает список часовых поясов из shared
библиотеки.
Просмотр вкладок
TabView
является эквивалентом SwiftUI для Jetpack ComposeBottomNavigation
. Вы можете использовать его для отображения панели вкладок в нижней части экрана и позволяет пользователю переключаться между различными представлениями приложения.
Вернувшись в contentView.swift, определите body
следующим образом:
var body: some View {
// 1
TabView {
// 2
TimezoneView()
// 3
.tabItem {
Label("Time Zones", systemImage: "network")
}
// 4
// FindMeeting()
// .tabItem {
// Label("Find Meeting", systemImage: "clock")
// }
}
.accentColor(Color.white)
// 5
.environmentObject(timezoneItems)
}
- Создайте SwiftUI
TabView
. - Первой вкладкой будет та
TimezoneView
, которую вы создадите следующей. - Примените значок
tabItem
с системной сетью и надписью Часовые пояса. - Второй вкладкой будет
FindMeeting
представление, которое вы еще не создали. (На данный момент это прокомментировано.) - Установите
timezoneItems
объект какenvironmentObject
.
Существует несколько способов передачи объектов в разные представления. Здесь вы проходите timezoneItems
через объект среды. Пользователи этого объекта, то есть любого дочернего представления, объявят @EnvironmentObject
переменную, которая получит этот объект.
Просмотр часового пояса
Щелкните правой кнопкой мыши в папке iosApp и выберите Новый файл .…

Затем выберите файл SwiftUI и нажмите Далее:

Затем сохраните как TimezoneView.swift:

Внутри файла сначала добавьте импорт для общей библиотеки:
import shared
Внутри структуры TimezoneView добавьте следующие переменные:
// 1
@EnvironmentObject private var timezoneItems: TimezoneItems
// 2
private var timezoneHelper = TimeZoneHelperImpl()
// 3
@State private var currentDate = Date()
// 4
let timer = Timer.publish(every: 1000, on: .main, in: .common).autoconnect()
// 5
@State private var showTimezoneDialog = false
- Это
timezoneItems
объект, переданный изContentView
. - Создайте экземпляр
timezoneHelper
. - Получить текущую дату.
- Создайте таймер для обновления каждую секунду.
- Переменная состояния, определяющая, показывать ли диалоговое окно с часовым поясом.
@State
используется с простыми типами структур, и его состояние сохраняется между перерисовками. Любая @State
оболочка свойства означает, что текущее представление владеет этими данными. SwiftUI отслеживает, когда @State
изменяется эта переменная, и перерисовывает представление при изменении ее значения.
@StateObject
используется с классами. В основном вы увидите@State
, что используемые представления SwiftUI — это struct
s.
Text("Hello, World")
Заменить следующим кодом:
// 1
NavigationView {
// 2
VStack {
// 3
TimeCard(timezone: timezoneHelper.currentTimeZone(),
time: DateFormatter.short.string(from: currentDate),
date: DateFormatter.long.string(from: currentDate))
Spacer()
// TODO: Add List
} // VStack
// 4
.onReceive(timer) { input in
currentDate = input
}
.navigationTitle("World Clocks")
// TODO: Add toolbar
} // NavigationView
- A
NavigationView
позволяет отображать новые экраны с заголовком и анимировать представление. - A
VStack
— вертикальный стек. Это в основном то же самое, что и столбец в JC. - Позвоните в
TimeCard
класс, чтобы показать часовой пояс в удобном формате карты. Используйте расширенияshort
иlong
DateFormatter изUtils
класса. - Используй свой таймер. Каждый раз, когда таймер меняется, обновляйте дату, которая затем обновит другие элементы.
Если вы посмотрите на файл Utils.swift, вы увидите определение полей short
long
DateFormatter
расширения и . Продолжайте и запустите приложение. Вот как это будет выглядеть:

Список часовых поясов
Затем замените // TODO: Add List
на:
// 1
List {
// 2
ForEach(Array(timezoneItems.selectedTimezones), id: \.self) { timezone in
// 3
NumberTimeCard(timezone: timezone,
time: timezoneHelper.getTime(timezoneId: timezone),
hours: "\(timezoneHelper.hoursFromTimeZone(otherTimeZoneId: timezone)) hours from local",
date: timezoneHelper.getDate(timezoneId: timezone))
.withListModifier()
} // ForEach
// 4
.onDelete(perform: deleteItems)
} // List
// 5
.listStyle(.plain)
Spacer()
- Создайте набор
List
элементов. - Создайте массив выбранных часовых поясов и создайте карточку для каждого из них.
- Покажите часовой пояс в красивой временной карте. Используйте пользовательский модификатор списка, чтобы удалить разделитель строк и вставки. (См. ListModifier.swift.)
- Добавьте возможность проводить пальцем для удаления. Вы определите
deleteItems
метод позже. - Сделайте стиль списка простым.
Это ForEach
специальная структура представления SwiftUI, которая может быть возвращена как a View
, в отличие от обычной forEach()
функции.
Далее, // TODO: Add toolbar
со следующим кодом:
// 1
.toolbar {
// 2
ToolbarItem(placement: .navigationBarTrailing) {
// 3
Button(action: {
showTimezoneDialog = true
}) {
Image(systemName: "plus")
.frame(alignment: .trailing)
.foregroundColor(.black)
}
} // ToolbarItem
} // toolbar
- Добавьте элемент панели инструментов в NavigationView.
- Поместите его на заднюю кромку (правая сторона для языков, которые читаются слева направо).
- Создайте кнопку со знаком плюс, которая установит
showTimezoneDialog
для переменной значение true.
Затем добавьте следующий код после// NavigationView
:
.fullScreenCover(isPresented: $showTimezoneDialog) {
TimezoneDialog()
.environmentObject(timezoneItems)
}
fullScreenCover
это способ представления полноэкранного модального представления поверх вашего текущего представления. При этом диалоговое окно часового пояса будет отображаться в виде полноэкранного окна. Поскольку это модально, должен быть способ отклонить его. Итак, для этого в диалоговом окне есть кнопка «Отклонить».
Кнопка на панели инструментов устанавливает showTimezoneDialog
для переменной значение true, которое является переменной состояния, управляемой SwiftUI. Когда это значение изменяется, отображается полноэкранный режим.
Затем добавьте deleteItems
метод после var body
кода:
func deleteItems(at offsets: IndexSet) {
let timezoneArray = Array(timezoneItems.selectedTimezones)
for index in offsets {
let element = timezoneArray[index]
timezoneItems.selectedTimezones.remove(element)
}
}
Приведенный выше код просматривает индексы вIndexSet
, находит выбранный часовой пояс и удаляет его из выбранного списка. Создайте и запустите приложение. Нажмите кнопку + вверху.
Вы увидите:

Попробуйте выполнить поиск по вашим любимым часовым поясам, выберите часовой пояс, а затем выполните поиск еще раз.
Когда вы будете искать Нью-Йорк, вот что вы увидите:

Когда вы закончите, нажмите кнопку «Отклонить«.
Вот как это выглядит с Нью-Йорком и Лиссабоном:

Если вы хотите удалить часовой пояс, просто проведите пальцем влево:

График работы
Вы захотите показать часы, которые доступны для встречи. Вы можете сделать это, показав часы работы на листе, который в данном случае является модальным диалоговым окном). Это простое представление со списком часов и кнопкой увольнения. Создайте новое представление SwiftUI с именем HourSheet.swift в папке iosApp. Удалите Text
представление, а затем добавьте следующие две переменные в самом начале представления:
@Binding var hours: [Int]
@Environment(\.presentationMode) var presentationMode
Первая переменная — это массив часов, которые будет передавать вызывающий абонент. Второй из них — это showHoursDialog
Boolean
. Это скроет диалоговое окно, установив для этой переменной значение false. Добавьте следующее внутриbody
:
// 1
NavigationView {
// 2
VStack {
// 3
List {
// 4
ForEach(hours, id: \.self) { hour in
Text("\(hour)")
}
} // List
} // VStack
.navigationTitle("Found Meeting Hours")
// 5
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Dismiss")
.frame(alignment: .trailing)
.foregroundColor(.black)
}
} // ToolbarItem
} // toolbar
} // NavigationView
- Используйте a
NavigationView
для отображения панели инструментов. - Используйте a
VStack
для названия. - Используйте a
List
для отображения каждого часа. - Используйте
ForEach
представление, чтобы отобразить текстовое представление для каждого часа. - Показать панель инструментов с кнопкой «Закрыть».
Это создает список для каждого часа и отображает его в Text
представлении. Чтобы заставить предварительный просмотр работать, измените HourSheet()
конструктор внутри HourSheet_Previews
на:
HourSheet(hours: .constant([8, 9, 10]))
Найти встречу
Следующий экран — это экран поиска собрания. На этом экране вы можете выбрать часы, в которые вы хотите искать встречи, а затем найти часы, которые подходят для всех. Создайте новый файл представления SwiftUI с именем FindMeeting.swift.
Сначала добавьте shared
импорт:
import shared
Затем удалите Text("Hello, World")
и добавьте следующие переменные:
// 1
@EnvironmentObject private var timezoneItems: TimezoneItems
// 2
private var timezoneHelper = TimeZoneHelperImpl()
// 3
@State private var meetingHours: [Int] = []
@State private var showHoursDialog = false
// 4
@State private var startDate = Calendar.current.date(bySettingHour: 8, minute: 0, second: 0, of: Date())!
@State private var endDate = Calendar.current.date(bySettingHour: 17, minute: 0, second: 0, of: Date())!
- Создайте
timezoneItems
переменную среды. Это будет получено из contentView. - Создайте экземпляр
TimeZoneHelperImpl
класса. - Список часов встреч, в которые все могут встретиться.
- Даты начала и окончания — 8 утра и 5 вечера.
Это дает нам все переменные, которые вам понадобятся для вашего экрана. Теперь вы можете начать работу над body
. Добавьте следующий код внутриbody
:
NavigationView {
VStack {
Spacer()
.frame(height: 8)
// TODO: Add Form
} // VStack
.navigationTitle("Find Meeting Time")
// TODO: Add sheet
} // NavigationView
Это будет вертикальный стек с видом навигации, который имеет заголовок и несколько разделителей вокруг заголовка. Теперь добавьте форму, состоящую из двух разделов: временной диапазон с начальным и конечным временем выбора и список выбранных часовых поясов. Заменить TODO: Add Form
на:
Form {
Section(header: Text("Time Range")) {
// 1
DatePicker("Start Time", selection: $startDate, displayedComponents: .hourAndMinute)
// 2
DatePicker("End Time", selection: $endDate, displayedComponents: .hourAndMinute)
}
Section(header: Text("Time Zones")) {
// 3
ForEach(Array(timezoneItems.selectedTimezones), id: \.self) { timezone in
HStack {
Text(timezone)
Spacer()
}
}
}
} // Form
// TODO: Add Button
- Средство выбора даты и времени начала.
- Средство выбора даты окончания времени.
- Список выбранных часовых поясов.
Теперь появляется кнопка, которая выполняет расчет часового пояса. Он вызовет метод shared
библиотекиsearch
. Заменить // TODO: Add Button
на:
Spacer()
Button(action: {
// 1
meetingHours.removeAll()
// 2
let startHour = Calendar.current.component(.hour, from: startDate)
let endHour = Calendar.current.component(.hour, from: endDate)
// 3
let hours = timezoneHelper.search(
startHour: Int32(startHour),
endHour: Int32(endHour),
timezoneStrings: Array(timezoneItems.selectedTimezones))
// 4
let hourInts = hours.map { kotinHour in
Int(truncating: kotinHour)
}
meetingHours += hourInts
// 5
showHoursDialog = true
}, label: {
Text("Search")
.foregroundColor(Color.black)
})
Spacer()
.frame(height: 8)
- Очистите свой массив от любых предыдущих значений.
- Получите часы начала и окончания.
- Вызовите метод поиска в общей библиотеке, преобразуя часы в целые числа.
- Создайте еще один массив целых чисел из возвращенных часов. Конвертировать в целые числа iOS.
- Установите флажок, чтобы показывать диалоговое окно «Часы».
Обратите внимание, что происходит небольшое преобразование. Вам нужно преобразовать Swift Int в 32-битный Int для Kotlin. Затем, когда вы получите значение обратно из shared
библиотеки, вам нужно преобразовать значения обратно в Swift Int. Теперь, когда кнопка устанавливает флаг для отображения диалогового окна часов, вам нужен способ отображения этого диалогового окна. Вы будете использовать лист — тип диалогового окна, который отображается в нижней части экрана. Заменить // TODO: Add sheet
на:
.sheet(isPresented: $showHoursDialog) {
HourSheet(hours: $meetingHours)
}
Вы почти на месте. Наконец, вам нужно добавить вкладку «Найти собрание» в TabView.
Просмотр содержимого
Вернитесь в contentView и снимите комментарии с FindMeeting
раздела.
Создайте и запустите приложение. Попробуйте добавить несколько часовых поясов. Перейдите на страницу FindMeeting, нажмите кнопку Поиска и посмотрите, отображаются ли какие-либо часы. Если у вас возникли проблемы и вы не видите никаких часов, начните с одного часового пояса и постепенно переходите к другим. Вполне возможно, что совместимых часов не существует. Попробуйте увеличить время окончания до 17 или 19. Это увеличит дальность действия. Вот пример часов между часовыми поясами Лос-Анджелеса и Нью-Йорка:

Поздравляю! Теперь у вас есть приложение для Android и iOS, которым вы можете похвастаться перед своими друзьями.
Ключевые моменты
- SwiftUI — это новый декларативный способ создания пользовательских интерфейсов для платформ Apple.
- Вы можете использовать Xcode или Android Studio для разработки своего кода SwiftUI.
- Используйте
@State
,@StateObject
,@ObservedObject
и@EnvironmentObject
для удержания состояния. - Используйте представления SwiftUI, такие как
VStack
,HStack
,NavigationView
иText
для создания своих пользовательских интерфейсов. - Используйте
List
представления для отображения множества элементов. ForEach
может возвращать представление, которое вы можете использовать внутриList
, а также другие представления.- Используйте
sheet
иfullScreenCover
для модальных экранов диалогового типа. - Используется
Int32
для преобразования целых чисел для Kotlin. - Используется
Int
для преобразования целых чисел Kotlin в целые числа Swift.
Куда идти дальше?
Чтобы узнать о:
Xcode: https://developer.apple.com/xcode /
СвифтУИ:
- Книга ученика SwiftUI:
- Официальная документация SwiftUI: https://developer.apple.com/documentation/swiftui /
- документация @StateObject: https://developer.apple.com/documentation/swiftui/stateobject
Поздравляю! Вы написали приложение SwiftUI, которое использует общую библиотеку для бизнес-логики. Теперь, когда у вас есть приложения для Android и iOS, в следующей главе будет показано, как создать приложение для macOS.