SwiftUI уроки (часть 12)

0bd14e0b2aeaafb3c26d1b44ccd6aa2b.png

ссылка на 11-ю часть

Работаем с Form и учимся использовать Picker, Stepper и Toggle

Практически в любом приложении мы в том или ином виде встречается с формами, под формами я имею ввиду такие объекты которые группируют в себе какие то данные которые вы заполняете или выставляете — например для настройки приложения или может быть для сохранения какой-то полезной для себя информации, такие формы позволяют пользователю интуитивно двигаться по интерфейсу и сразу цеплять глазом то, что именно они хотели бы заполнить или получить. Для разработчиков которые используют SwiftUI формы также стали очень удобным объектом, ведь количество секций в форме всегда конечно, всегда имеет понятные значения и кажется что использовать их вместо например таблиц просто выглядит более логичным, именно в таком контексте.

Сегодня мы с вами посмотрим на эти самые формы и научимся с ними работать, так как тема это довольно простая — мы заодно поработаем с Picker, Stepper и Toggle чтобы расширить свой кругозор относительно инструментов предоставляемых во фреймворке SUI.

В первую очередь скачайте подготовленный проект в котором уже есть скелет того на чём мы будем учиться дополняя его.

Ссылка на проект

Разархивируйте его и давайте я объясню что в нем есть на текущий момент.

Наше приложение это такая условная витрина автомобилей для аренды, у нас есть фото машины, ее вин номер, ее тип, имя и прочие данные.

Собственно сами данные вы можете увидеть в модели Car, в одноименном файле.

В файле FakeData у нас есть заготовка с объектами реализованными с помощью этой модели

Ну и наконец давайте повнимательнее посмотрим на те файлы которые связаны с SwiftUI

BasicImageRow:

  1. var car: Car — это свойство структуры, которое хранит объект типа Car, содержащий информацию об автомобиле в ячейке.

  2. var body: some View — вычисляемое свойство, которое определяет содержимое нашей ячейки.

  3. HStack — контейнер, который располагает дочерние элементы горизонтально.

  4. Image(car.image) — изображение автомобиля, которое загружается из ресурсов приложения.

  5. .frame(width: 60, height: 60) — модификатор, который устанавливает размер изображения.

  6. .clipShape(Circle()) — модификатор, который обрезает изображение в форме круга.

  7. .padding(.trailing, 10) — модификатор, который добавляет отступ справа от изображения.

  8. VStack(alignment: .leading) — контейнер, который располагает дочерние элементы вертикально и выравнивает их по левому краю.

  9. Text(car.name) — текстовое представление, которое отображает название автомобиля.

  10. Text(String(repeating: "$", count: car.priceLevel)) — это текстовое представление, которое отображает цену автомобиля (тип цены) в зависимости от уровня цены (priceLevel).

  11. Text(car.type.rawValue) — текстовое представление, которое отображает тип автомобиля.

  12. Text(car.vin) — текстовое представление, которое отображает VIN-номер автомобиля.

  13. if car.isBooked — это условная проверка, которая отображает изображение, если автомобиль забронирован.

  14. Image(systemName: "checkmark.seal.fill") — это изображение, которое отображает галочку, если автомобиль забронирован.

  15. .foregroundColor(.red) — это модификатор, который устанавливает цвет галочки.

  16. if car.isFavorite — это условная проверка, которая отображает изображение, если автомобиль добавлен в избранное.

  17. Image(systemName: "star.fill") — это изображение, которое отображает звезду, если автомобиль добавлен в избранное.

И наконец ContentView:

  1. @State var cars = FakeData.cars — свойство структуры, которое хранит массив объектов типа Car, содержащий информацию об автомобилях. @State — это имя атрибута, которое указывает, что свойство может изменяться и что приложение должно перерисовывать представление при изменении свойства.

  2. @State private var selectedCar: Car? — свойство структуры, которое хранит выбранный автомобиль.

  3. NavigationStack — контейнер навигации.

  4. List — контейнер, который отображает список элементов.

  5. ForEach(cars) { car in — это цикл, который перебирает все элементы массива cars и выполняет код внутри цикла для каждого элемента (в нашем случае чтобы отображать ячейки).

  6. BasicImageRow(car: car) — это ячейка, которая отображает информацию об автомобиле.

  7. .contextMenu — модификатор, который отображает контекстное меню при долгом нажатии на элемент списка.

  8. Button(action: { self.book(item: car) }) — кнопка, которая вызывает метод book(item:) при нажатии на нее.

  9. HStack — горизонтальный контейнер.

  10. Text("Забронировать") — текстовое представление, которое отображает текст кнопки.

  11. Image(systemName: "checkmark.seal.fill") — изображение, которое отображает галочку.

  12. Button(action: { self.delete(item: car) }) — это кнопка, которая вызывает метод delete(item:) при нажатии на нее.

  13. Text("Удалить") — текстовое представление, которое отображает текст кнопки удаления.

  14. Image(systemName: "trash") — это изображение, которое отображает мусорную корзину.

  15. Button(action: { self.setFavorite(item: car) }) — кнопка, которая вызывает метод setFavorite(item:) при нажатии на нее.

  16. Text("Любимый") — это текстовое представление, которое отображает текст кнопки добавляющей в любимые автомобили.

  17. Image(systemName: "star") — это изображение, которое отображает звезду.

  18. .onTapGesture — модификатор, который вызывает код при нажатии на элемент списка.

  19. self.selectedCar = car — это код, который выбирает автомобиль при нажатии на него.

  20. .onDelete — это модификатор, который вызывает код при удалении элемента списка.

  21. self.cars.remove(atOffsets: indexSet) — это код, который удаляет выбранные элементы из массива cars.

  22. .navigationTitle("Аренда Авто") — это модификатор, который устанавливает заголовок навигации.

Методы delete(item:), setFavorite(item:) и book(item:) выполняют соответствующие действия с выбранным автомобилем. Метод delete(item:) удаляет автомобиль из массива cars, метод setFavorite(item:) отмечает автомобиль как любимый, а метод book(item:) бронирует автомобиль.

Можете еще установить приложение на симулятор и немного с ним поиграть, чтобы понять что тут к чему.

3cdf3aa5c7517a82830aad34a7d4383d.gif

Отлично, с первичной установкой приложения разобрались. Давайте переходить к самому интересному.

Создаем Form UI

Как упоминалось ранее, SwiftUI предоставляет UI-компонент под названием Form для создания интерфейсов форм. Это контейнер, который удерживает и группирует элементы управления (например переключатели) для ввода данных. Вместо того чтобы объяснять его использование, лучше сразу перейти к реализации. Так вы быстрее поймете, как использовать этот компонент.

Так как мы создадим отдельный экран для настроек, давайте создадим новый файл для нашей Form. В проводнике проекта щелкните правой кнопкой мыши на папке SwiftUIForm и выберите «New File…». Затем выберите шаблон SwiftUI View и назовите файл SettingView.swift.

10373f43c9d16bca4068541b38e7873b.png

Итак, давайте создавать форму. Замените всё находящее внутри файла на следующий код:

import SwiftUI

struct SettingView: View {
	var body: some View {
		NavigationStack {
			Form {
				Section(header: Text("Сортировать")) {
					Text("Отобразить заказ")
				}
				Section(header: Text("Отфильтровать")) {
					Text("Фильтры")
				}
			}
			.navigationBarTitle("Настройки")
		}
	}
}

struct SettingView_Previews: PreviewProvider {
	static var previews: some View {
		SettingView()
	}
}

Для создания формы мы использовали контейнер Form. Внутри него добавляются секции и компоненты формы (текстовые поля, списки, переключатели и т.д.). В приведенном коде мы создаем две секции: «Предпочтения сортировки» и «Предпочтения по фильтрации». В каждой секции находится текстовое представление.

67b8fc727687939143d1ed29d5e903ef.png

Создаем Picker View
Сама по себе форма без сбора информации вещь абсолютная бесполезная, поэтому просто отображать компонент Text мы разумеется не будем. В реальной форме мы используем три типа UI-контролов для ввода данных: picker view, toggle и stepper. Начнем с предпочтений сортировки. Для этого мы реализуем picker view.

Пользователям предоставляется возможность выбрать порядок отображения списка машин. Мы предлагаем три варианта на выбор:

  1. По алфавиту

  2. Сначала показывать избранные

  3. Сначала показывать забронированные

Контроллер Picker отлично подходит для обработки такого ввода. Сначала мы используем массив для представления каждого из указанных вариантов. Давайте объявим массив под названием displayOrders в SettingView:

	private var displayOrders = [ "Алфитный порядок",
								  "Показывать сначала избранные",
								  "Показывать сначала забронированные"]

Для использования picker вам также нужно объявить переменную состояния, чтобы хранить выбранный пользователем вариант. В SettingView объявите переменную следующим образом:

@State private var selectedOrder = 0

В данном случае 0 означает выбранный в текущий момент формат отображения, давайте теперь заменим код внутри Секции связанной с сортировкой

				Picker(selection: $selectedOrder, label: Text("Формат отображения")) {
					ForEach(0 ..< displayOrders.count, id: \.self) {
						Text(self.displayOrders[$0])
					}
				}

365bdcba1ea19ee9cf5d15fe9c67d548.png


Таким образом мы создаем контейнер picker в SwiftUI. Вам нужно предоставить два значения: привязку выбора (т.е. $selectedOrder) и текстовую метку, описывающую назначение опции. В замыкании вы отображаете доступные варианты с помощью Text.
На холсте вы увидите, что порядок отображения установлен на «Алфавитный порядок». Это потому, что selectedOrder по умолчанию равен 0. Если вы нажмете кнопку «Play» для тестирования, нажатие на опцию откроет меню, где будут показаны все доступные варианты. Вы можете выбрать любой из них (например, «Показывать сначала избранные») для тестирования. При этом после выбора — порядок отображения изменится на ваш выбор.

d6c0b6167af0e91053dd9c63052e49ba.png

Работаем с переключателями

Теперь давайте перейдем к вводу для настройки предпочтений фильтрации. Сначала реализуем переключатель Toggle для включения/выключения фильтра «Показывать только забронированные».

Переключатель имеет только два состояния: ВКЛ и ВЫКЛ. Этот элемент управления полезен для предложений пользователям выбрать между двумя взаимоисключающими опциями.
Создание переключателя с использованием SwiftUI достаточно просто. Как и для Picker, нам нужно объявить переменную состояния, чтобы хранить текущее значение переключателя. Поэтому объявите следующую переменную в SettingView:

@State private var showBookedOnly = false

И теперь давайте обновим секцию связанную с фильтрацией следующим образом:

				Section(header: Text("Отфильтровать")) {
					Toggle(isOn: $showBookedOnly) {
						Text("Показывать только забронированные")
					}
				}

Для создания переключателя используйте Toggle и передайте ему текущее состояние переключателя (в нашем случае это showBookedOnly). В замыкании вы указываете описание переключателя, чтобы пользователь понимал за что будет отвечать такой рубильник. Для этого можно использовать просто — Text view.

На вьюшке должен появиться переключатель в секции фильтрации. Если вы протестируете приложение, вы сможете переключать его между состояниями ВКЛ и ВЫКЛ. Аналогично, переменная состояния showBookedOnly будет постоянно отслеживать выбор пользователя.

ee967a2da9ffbcf5d5c4bee9c4d94406.png

Используем Stepper

Последний UI-контрол в нашей форме настроек — это Stepper. Мы хотим сделать так, чтобы пользователи могли фильтровать автомобили по уровню цены. Каждый автомобиль имеет индикатор цены с диапазоном от 1 до 5. Пользователи могут регулировать уровень цены, чтобы сузить количество отображаемых автомобилей в списке.

В форме настроек мы реализуем Stepper, чтобы пользователи могли настраивать этот параметр. В iOS Stepper отображает текстовое поле и кнопки плюс и минус для увеличения и уменьшения значения в текстовом поле.

Для реализации Stepper в SwiftUI, нам сначала нужно объявить переменную состояния для хранения текущего значения. В данном случае эта переменная хранит фильтр уровня цены пользователя. Объявите переменную состояния в SettingView следующим образом:

@State private var maxPriceLevel = 5

По умолчанию, мы устанавливаем maxPriceLevel равным 5 — что отобразит абсолютно все машины. Давайте теперь обновим нашу секцию с фильтрацией следующим образом:

				Section(header: Text("Отфильтровать")) {
					Toggle(isOn: $showBookedOnly) {
						Text("Показывать только забронированные")
					}

					Stepper(value: $maxPriceLevel, in: 1...5) {
						Text("Отобразить автомобили в ценновой категории")
							.font(.subheadline)
							.foregroundStyle(.black)
						Text(String(repeating: "$", count: maxPriceLevel))
							.font(.callout)
							.foregroundStyle(.green)
						Text("или ниже")
							.font(.subheadline)
							.foregroundStyle(.black)
					}
				}

Обратите внимание, в секции нам не нужно помещать разные View в контейнеры вроде VStack для отображения по горизонтали, в этом и суть форм и секций, что они логично отделяются друг от друга в вашем контейнере.

Итак, мы создали Stepper через инициализатор принимающий наше Binding свойство, диапазон чисел в котором может ходить степпер и собственно клоужер в котором мы отображаем текстовые представления — чтобы пользователь понимать что конкретно будет меняться если он будет нажимать на плюс или минус.

Попробуйте протестируйте экран сами

40c14fe9fb30c8ff46b68bd87769dee5.png

Отображаем нашу Форму

Теперь, когда мы завершили создание интерфейса формы, следующий шаг — наконец показать эту форму пользователям. Мы отобразим эту форму в виде модального окна, заодно повторим предыдущие полученные знания. В ContentView добавим кнопку «Настройки» на панель навигации, чтобы открыть экран настроек.

Перейдите в ContentView.swift. Сначала нам нужно объявить переменную State для отслеживания состояния модального окна (показано или не показано). Добавьте следующую строку кода для объявления переменной состояния:

@State private var showSettings: Bool = false

Затем добавьте следующие модификаторы в NavigationStack (разместите их после navigationTitle):

.toolbar {
    ToolbarItem(placement: .navigationBarTrailing) {
        Button(action: {
            self.showSettings = true
        }, label: {
            Image(systemName: "gear").font(.title2)
                .foregroundColor(.black)
        })
    }
}
.sheet(isPresented: $showSettings) {
    SettingView()
}

7fa5d04dfd75dbcaf97624bf50776fa4.png

Модификатор toolbar позволяет добавить кнопку на панель навигации. Вы можете создать кнопку в начале или в конце панели навигации. Так как мы хотим отобразить кнопку в правом верхнем углу, используем параметр navigationBarTrailing. Модификатор sheet используется для отображения SettingView в виде модального окна.

На холсте вы должны увидеть значок шестеренки на панели навигации. Нажмите на значок шестеренки, и это должно открыть экран настроек.

One More Thing

На прошлом уроке мы с вами добавляли кнопку на наш модальный экран, будет здорово если вы самостоятельно добавите две кнопки (сохранить и закрыть) на нашем экране настроек. В принципе можете даже не накручивать на них логику, ну как минимум на кнопку сохранения. (Да, это то чем мы будем заниматься в следующем уроке)

Попробуйте сделать это самостоятельно, но если не получилось, то ниже код как это сделать:

import SwiftUI

struct SettingView: View {
	@Environment(\.dismiss) var dismiss

	private var displayOrders = [ "Алфавитный порядок",
								  "Показывать сначала избранные",
								  "Показывать сначала забронированные"]

	@State private var selectedOrder = 0
	@State private var showBookedOnly = false
	@State private var maxPriceLevel = 5

	var body: some View {
		NavigationStack {
			Form {
				Picker(selection: $selectedOrder, label: Text("Формат отображения")) {
					ForEach(0 ..< displayOrders.count, id: \.self) {
						Text(self.displayOrders[$0])
					}
				}
				Section(header: Text("Отфильтровать")) {
					Toggle(isOn: $showBookedOnly) {
						Text("Показывать только забронированные")
					}

					Stepper(value: $maxPriceLevel, in: 1...5) {
						Text("Отобразить автомобили в ценновой категории")
							.font(.subheadline)
							.foregroundStyle(.black)
						Text(String(repeating: "$", count: maxPriceLevel))
							.font(.callout)
							.foregroundStyle(.green)
						Text("или ниже")
							.font(.subheadline)
							.foregroundStyle(.black)
					}
				}
			}
			.navigationBarTitle("Настройки")
			.toolbar {
				ToolbarItem(placement: .navigationBarLeading) {
					Button(action: {
						dismiss()
					}) {
						Text("Закрыть")
							.foregroundStyle(.black)
					}
				}
				ToolbarItem(placement: .navigationBarTrailing) {
					Button(action: {
						// здесь будем добавлять сохранение
					}) {
						Text("Сохранить")
							.foregroundStyle(.black)
					}
				}
			}
		}
	}
}

struct SettingView_Previews: PreviewProvider {
	static var previews: some View {
		SettingView()
	}
}

Ну и наконец можно запустить приложение и попробовать его в деле

1de8d9b71eeb8e7e372757180bdd0e36.gif

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

Поэтому как и прежде подписывайтесь на мой телеграм канал — https://t.me/swiftexplorer

И приходите за новыми статьями, буду рад вашим комментариям и лайкам!

Спасибо за прочтение!

© Habrahabr.ru