Основы Kotlin Multiplatform. Бесплатный учебник на русском языке (KA0700)
Основы Kotlin Multiplatform. Бесплатный учебник на русском языке (KA0700)

KA0704 — Разработка пользовательского интерфейса: iOS SwiftUI

  • Запись изменена:20.12.2022
  • Post category:Публикации
  • Reading time:23 минут чтения

Как вы узнали из предыдущей главы, 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. На значке может появиться красный крестик.

Рис. 4.1 - Конфигурация Android Studio iosApp
Рис. 4.1 — Конфигурация Android Studio iosApp

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

Рис. 4.2 - Конфигурация редактирования Android Studio
Рис. 4.2 — Конфигурация редактирования Android Studio

Выберите телефон и цель, например iPhone 13 | iOS 15.0, и нажмите кнопку ОК.

Теперь щелкните значок молотка (или нажмите Command-F9), чтобы построить. Это создаст общую структуру, необходимую для iOS.

Рис. 4.3 - Кнопка сборки Android Studio
Рис. 4.3 — Кнопка сборки Android Studio

Xcode

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

Рис. 4.4 - Диалоговое окно Xcode Открыть проект
Рис. 4.4 — Диалоговое окно Xcode Открыть проект

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

Рис. 4.5 - Боковая панель файлов проекта Xcode
Рис. 4.5 — Боковая панель файлов проекта Xcode

Текущая система пользовательского интерфейса

В iOS вы обычно используете раскадровки или создаете свой пользовательский интерфейс в коде, если разрабатываете свой пользовательский интерфейс в UIKit. Под этими раскадровками находится сложный XML-файл. Несмотря на то, что редактор макетов в Xcode хорош, все равно требуется немало работы для разработки, а затем подключения к коду. SwiftUI — это декларативная система пользовательского интерфейса, полностью написанная в коде. Никаких макетов или раскадровок. Он намного проще в использовании и позволяет многократно использовать код с меньшими представлениями. Xcode предоставляет предварительные просмотры, чтобы вы могли создавать небольшие компоненты и просматривать их рядом с редактором.

Знакомство со SwiftUI

При создании проекта с помощью плагина KMM создаются два файла Swift: contentView.swift и iOSApp.swift. Эти два файла похожи на приложение “Hello World”. Они показывают текстовое поле в центре экрана со словом “Привет”. Плагин добавляет несколько файлов, чтобы упростить разработку.

Приложение

Откройте iOSApp.swift:

Рис. 4.6 - iOSApp.swift
Рис. 4.6 — 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
    }
}

Создайте приложение из меню продукта.

Рис. 4.7 - Пункт меню сборки Xcode
Рис. 4.7 — Пункт меню сборки Xcode

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

Рис. 4.8 - Кнопка запуска Xcode
Рис. 4.8 — Кнопка запуска Xcode

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

Рис. 4.9 - Начальный экран на iOS
Рис. 4.9 — Начальный экран на iOS

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

Рис. 4.10 - Список настроек Android Studio
Рис. 4.10 — Список настроек Android Studio

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

Рис. 4.11 - Кнопка запуска Android Studio
Рис. 4.11 — Кнопка запуска Android Studio

Просмотр содержимого

Откройте 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)
  }
  1. Создайте SwiftUI TabView.
  2. Первой вкладкой будет таTimezoneView, которую вы создадите следующей.
  3. Примените значок tabItemс системной сетью и надписью Часовые пояса.
  4. Второй вкладкой будет FindMeetingпредставление, которое вы еще не создали. (На данный момент это прокомментировано.)
  5. Установите timezoneItemsобъект как environmentObject.

Существует несколько способов передачи объектов в разные представления. Здесь вы проходите timezoneItemsчерез объект среды. Пользователи этого объекта, то есть любого дочернего представления, объявят @EnvironmentObjectпеременную, которая получит этот объект.

Просмотр часового пояса

Щелкните правой кнопкой мыши в папке iosApp и выберите Новый файл .

Рис. 4.12 - Параметры файла Xcode
Рис. 4.12 — Параметры файла Xcode

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

Рис. 4.13 - Диалоговое окно Xcode для создания нового типа файла
Рис. 4.13 — Диалоговое окно Xcode для создания нового типа файла

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

Рис. 4.14 - Диалоговое окно создания имени файла в Xcode
Рис. 4.14 — Диалоговое окно создания имени файла в Xcode

Внутри файла сначала добавьте импорт для общей библиотеки:

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
  1. Это timezoneItemsобъект, переданный из ContentView.
  2. Создайте экземпляр timezoneHelper.
  3. Получить текущую дату.
  4. Создайте таймер для обновления каждую секунду.
  5. Переменная состояния, определяющая, показывать ли диалоговое окно с часовым поясом.

@State используется с простыми типами структур, и его состояние сохраняется между перерисовками. Любая @Stateоболочка свойства означает, что текущее представление владеет этими данными. SwiftUI отслеживает, когда @Stateизменяется эта переменная, и перерисовывает представление при изменении ее значения.

@StateObject используется с классами. В основном вы увидите@State, что используемые представления SwiftUI — это structs.

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
  1. NavigationViewпозволяет отображать новые экраны с заголовком и анимировать представление.
  2. VStack— вертикальный стек. Это в основном то же самое, что и столбец в JC.
  3. Позвоните в TimeCardкласс, чтобы показать часовой пояс в удобном формате карты. Используйте расширения shortи longDateFormatter из Utilsкласса.
  4. Используй свой таймер. Каждый раз, когда таймер меняется, обновляйте дату, которая затем обновит другие элементы.

Если вы посмотрите на файл Utils.swift, вы увидите определение полей shortlong DateFormatterрасширения и . Продолжайте и запустите приложение. Вот как это будет выглядеть:

Рис. 4.15 - Экран мировых часов
Рис. 4.15 — Экран мировых часов

Список часовых поясов

Затем замените // 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()
  1. Создайте набор Listэлементов.
  2. Создайте массив выбранных часовых поясов и создайте карточку для каждого из них.
  3. Покажите часовой пояс в красивой временной карте. Используйте пользовательский модификатор списка, чтобы удалить разделитель строк и вставки. (См. ListModifier.swift.)
  4. Добавьте возможность проводить пальцем для удаления. Вы определите deleteItemsметод позже.
  5. Сделайте стиль списка простым.

Это 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
  1. Добавьте элемент панели инструментов в NavigationView.
  2. Поместите его на заднюю кромку (правая сторона для языков, которые читаются слева направо).
  3. Создайте кнопку со знаком плюс, которая установит 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, находит выбранный часовой пояс и удаляет его из выбранного списка. Создайте и запустите приложение. Нажмите кнопку + вверху.

Вы увидите:

Рис. 4.16 - Экран поиска часовых поясов
Рис. 4.16 — Экран поиска часовых поясов

Попробуйте выполнить поиск по вашим любимым часовым поясам, выберите часовой пояс, а затем выполните поиск еще раз.

Когда вы будете искать Нью-Йорк, вот что вы увидите:

Рис. 4.17 - Экран поиска часовых поясов
Рис. 4.17 — Экран поиска часовых поясов

Когда вы закончите, нажмите кнопку «Отклонить«.

Вот как это выглядит с Нью-Йорком и Лиссабоном:

Рис. 4.18 - Выбранные часовые пояса
Рис. 4.18 — Выбранные часовые пояса

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

Рис. 4.19 - Удаление выбранного часового пояса
Рис. 4.19 — Удаление выбранного часового пояса

График работы

Вы захотите показать часы, которые доступны для встречи. Вы можете сделать это, показав часы работы на листе, который в данном случае является модальным диалоговым окном). Это простое представление со списком часов и кнопкой увольнения. Создайте новое представление 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
  1. Используйте a NavigationViewдля отображения панели инструментов.
  2. Используйте a VStackдля названия.
  3. Используйте a Listдля отображения каждого часа.
  4. Используйте ForEachпредставление, чтобы отобразить текстовое представление для каждого часа.
  5. Показать панель инструментов с кнопкой «Закрыть».

Это создает список для каждого часа и отображает его в 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())!
  1. Создайте timezoneItemsпеременную среды. Это будет получено из contentView.
  2. Создайте экземпляр TimeZoneHelperImplкласса.
  3. Список часов встреч, в которые все могут встретиться.
  4. Даты начала и окончания — 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
  1. Средство выбора даты и времени начала.
  2. Средство выбора даты окончания времени.
  3. Список выбранных часовых поясов.

Теперь появляется кнопка, которая выполняет расчет часового пояса. Он вызовет метод 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)
  1. Очистите свой массив от любых предыдущих значений.
  2. Получите часы начала и окончания.
  3. Вызовите метод поиска в общей библиотеке, преобразуя часы в целые числа.
  4. Создайте еще один массив целых чисел из возвращенных часов. Конвертировать в целые числа iOS.
  5. Установите флажок, чтобы показывать диалоговое окно «Часы».

Обратите внимание, что происходит небольшое преобразование. Вам нужно преобразовать Swift Int в 32-битный Int для Kotlin. Затем, когда вы получите значение обратно из sharedбиблиотеки, вам нужно преобразовать значения обратно в Swift Int. Теперь, когда кнопка устанавливает флаг для отображения диалогового окна часов, вам нужен способ отображения этого диалогового окна. Вы будете использовать лист — тип диалогового окна, который отображается в нижней части экрана. Заменить // TODO: Add sheetна:

.sheet(isPresented: $showHoursDialog) {
  HourSheet(hours: $meetingHours) 
}

Вы почти на месте. Наконец, вам нужно добавить вкладку «Найти собрание» в TabView.

Просмотр содержимого

Вернитесь в contentView и снимите комментарии с FindMeetingраздела.

Создайте и запустите приложение. Попробуйте добавить несколько часовых поясов. Перейдите на страницу FindMeeting, нажмите кнопку Поиска и посмотрите, отображаются ли какие-либо часы. Если у вас возникли проблемы и вы не видите никаких часов, начните с одного часового пояса и постепенно переходите к другим. Вполне возможно, что совместимых часов не существует. Попробуйте увеличить время окончания до 17 или 19. Это увеличит дальность действия. Вот пример часов между часовыми поясами Лос-Анджелеса и Нью-Йорка:

Рис. 4.20 - Экран найденных часов собрания
Рис. 4.20 — Экран найденных часов собрания

Поздравляю! Теперь у вас есть приложение для Android и iOS, которым вы можете похвастаться перед своими друзьями.

Ключевые моменты

  • SwiftUI — это новый декларативный способ создания пользовательских интерфейсов для платформ Apple.
  • Вы можете использовать Xcode или Android Studio для разработки своего кода SwiftUI.
  • Используйте @State@StateObject@ObservedObjectи @EnvironmentObjectдля удержания состояния.
  • Используйте представления SwiftUI, такие как VStackHStackNavigationViewи Textдля создания своих пользовательских интерфейсов.
  • Используйте Listпредставления для отображения множества элементов.
  • ForEach может возвращать представление, которое вы можете использовать внутриList, а также другие представления.
  • Используйте sheetи fullScreenCoverдля модальных экранов диалогового типа.
  • Используется Int32для преобразования целых чисел для Kotlin.
  • Используется Intдля преобразования целых чисел Kotlin в целые числа Swift.

Куда идти дальше?

Чтобы узнать о:

Xcode: https://developer.apple.com/xcode /

СвифтУИ:

Поздравляю! Вы написали приложение SwiftUI, которое использует общую библиотеку для бизнес-логики. Теперь, когда у вас есть приложения для Android и iOS, в следующей главе будет показано, как создать приложение для macOS.