SwiftUI 3.0. Четвертая часть
В этой публикации мы поговорим о новой обертке над свойством @FocusState для работы с клавиатурой, позволяющей определить находится ли текстовое поле в фокусе или нет. Узнаем, как создавать List и ForEach на основе коллекций, которые в свою очередь имеют обертки @State или @Binding. Взглянем на новый метод Self._printChanges(), предназначенный исключительно для отладки, который можно использовать, чтобы определить, что послужило триггером для обновления представления. А так же рассмотрим прочие не очень большие, но интересные нововведения.
SwiftUI 3.0. Использование @FocusState для скрытия клавиатуры
Начиная с версии iOS 15 в SwiftUI появилась новая обертка над свойством @FocusState и модификатор focused(), который вызывается для текстового поля и хранит значение @FocusState свойства, позволяющее определить находится ли текстовое поле в фокусе или нет. А уже в зависимости от этого можно активировать или скрывать клавиатуру:
struct ContentView: View {
@State private var name = ""
@FocusState private var nameIsFocused: Bool
var body: some View {
VStack {
TextField("Enter your name", text: $name)
.focused($nameIsFocused)
Button("Submit") {
nameIsFocused = false
}
}
}
}
По нажатию на кнопку Submit клавиатура будет скрыта.
Давайте рассмотрим еще один пример в котором мы будем использовать обертку @FocusState для отслеживания опционального кейса перечисления, определяющего, какое поле формы в данный момент находится в фокусе. Определим для этого примера три текстовых поля, запрашивающих у пользователя различную информацию, а затем отправить форму, как только будет получена последняя часть:
struct ContentView: View {
enum Field {
case firstName
case lastName
case emailAddress
}
@State private var firstName = ""
@State private var lastName = ""
@State private var emailAddress = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("Enter your first name", text: $firstName)
.focused($focusedField, equals: .firstName)
.textContentType(.givenName)
.submitLabel(.next)
TextField("Enter your last name", text: $lastName)
.focused($focusedField, equals: .lastName)
.textContentType(.familyName)
.submitLabel(.next)
TextField("Enter your email address", text: $emailAddress)
.focused($focusedField, equals: .emailAddress)
.textContentType(.emailAddress)
.submitLabel(.join)
}
.onSubmit {
switch focusedField {
case .firstName:
focusedField = .lastName
case .lastName:
focusedField = .emailAddress
default:
print("Creating account…")
}
}
}
}
Важно: не следует пытаться использовать одну и ту же привязку фокуса для двух разных полей формы.
Если необходимо поддерживать iOS 14 и 13, то с этим будут сложности, так как для этих версий iOS нет готового способа из коробки.
В этом случае скрывать клавиатуру вам придется следующим способом:
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
Здесь мы обращаемся к UIKit, что бы выполнить поиск в так называемой цепочке респондентов - коллекции элементов управления, которые в настоящее время реагируют на ввод пользователя, - и найти тот, который способен отказаться от своего статуса первого респондента. Это причудливый способ сказать «попросите все, у кого есть контроль, прекратить использовать клавиатуру», что в нашем случае означает, что клавиатура будет скрыта.
Поскольку данное выражение тяжело читается, то для него лучше определить специальный метод, который можно имплементировать в расширении для View:
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
Теперь можно вызывать метод hideKeyboard() из любого представления SwiftUI:
struct ContentView: View {
@State private var tipAmount = ""
var body: some View {
VStack {
TextField("Name: ", text: $tipAmount)
.textFieldStyle(.roundedBorder)
.keyboardType(.decimalPad)
Button("Submit") {
print("Tip: \(tipAmount)")
hideKeyboard()
}
}
}
}
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
Важно: если вы используете для работы Xcode 12, то вместо .roundedBorder необходимо использовать RoundedBorderTextFieldStyle().
SwiftUI 3.0. Создание List и ForEach на основе связанных (байндинг) коллекций
Начиная с версии iOS 15 такие типы, как List и ForEach можно создавать на основе коллекций, которые в свою очередь имеют обертки @State или @Binding и использовать индивидуальные привязки к каждому элементу коллекции. Это нужно в тех случаях, когда отображаемый контент может иметь поля для редактирования. Это могут быть текстовые поля, которые необходимо заполнить или, например переключатель при помощи которого пользователь сможет определять состояние логического свойства:
struct User: Identifiable {
let id = UUID()
var name: String
var isContacted = false
}
struct ContentView: View {
@State private var users = [
User(name: "Taylor"),
User(name: "Justin"),
User(name: "Adele")
]
var body: some View {
List($users) { $user in
Text(user.name)
Spacer()
Toggle("User has been contacted", isOn: $user.isContacted)
.labelsHidden()
}
}
}
В этом примере мы создали список пользователей, где каждый пользователь имеет переключатель, определяющий был ли установлен с ним контакт. Взаимодействуя с этим элементом интерфейса мы меняем значение логического свойства isContacted для этого элемента коллекции.
Использование байндингов таким образом является наиболее оптимизированным способом изменения значений элементов списка, так как не приводит к полной перезагрузке всего представления при изменении значения одного элемента.
Swift3.0. Метод для отладки Self._printChanges()
В SwiftUI появился специальный метод Self._printChanges() предназначенный исключительно для отладки, который можно использовать, чтобы определить, что послужило триггером для обновления представления. Метод будет полезен в тех случаях, когда по какой-то причине представление повторно вызывает свое свойство body, а вы не знаете почему это происходит.
Давайте рассмотрим пример в котором представление полагается на наблюдаемый объект, случайным образом выдающий уведомления об изменениях:
class EvilStateObject: ObservableObject {
var timer: Timer?
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
if Int.random(in: 1...5) == 1 {
self.objectWillChange.send()
}
}
}
}
struct ContentView: View {
@StateObject private var evilObject = EvilStateObject()
var body: some View {
print(Self._printChanges())
return Text("What could possibly go wrong?")
}
}
Метод Self._printChanges() должен вызываться в теле свойства body. Это означает, что перед тем как вернуть окончательное представление необходимо явно прописать ключевое слово return.
У Питера Штейнбергера есть полезный совет, как определить повторный вызов свойства body у представления: назначьте случайный цвет фона одному из представлений. Если свойство body будет вызываться не один раз, то это сразу будет бросаться в глаза.
Чтобы реализовать это можно сделать расширение для типа Color, в котором мы будем получать случайны цвет:
extension ShapeStyle where Self == Color {
static var random: Color {
Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
}
А далее подставлять закрашивать в этот цвет фон любого представления при помощи модификатора background():
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.background(.random)
}
}
SwiftUI 3.0. Параметр value для модификатора animation()
Модификатор animation() с одним параметром теперь официально задеприкейчен, т.к. он переодически вызывал различное неожиданное поведение. Рассмотрим следующий пример:
struct ContentView: View {
@State private var scaledUp = true
var body: some View {
VStack {
Text("Hello, world!")
.scaleEffect(scaledUp ? 2 : 1)
.animation(.linear(duration: 2))
.onTapGesture { scaledUp.toggle() }
}
}
}
Тут все довольно просто: мы масштабируем текст в большую или меньшую сторону тапая по нему. Сама анимация при этом привязана к тексту и в итоге она срабатывает даже в те моменты, которые мы не предусматривали, например при смене ориентации устройства. В этом случае смещение текста от его текущего местоположения к центру происходит в течении 2-х секунд.
Теперь с доработанным модификатором animation(_:value:), который позволяет привязать анимацию к значению определенного свойства, такого поведения можно не ждать:
struct ContentView: View {
@State private var scaledUp = true
var body: some View {
VStack {
Text("Hello, world!")
.scaleEffect(scaledUp ? 2 : 1)
.animation(.linear(duration: 2), value: scaledUp)
.onTapGesture { scaledUp.toggle() }
}
}
}
Начиная с версии iOS 15 такой элемент интерфейса, как Toggle теперь можно отображать не в виде переключателя, а как кнопку, которая подсвечивается когда переключатель включен. За это отвечает новый стиль .button для модификатора toggleStyle:
struct ContentView: View {
@State private var isOn = false
var body: some View {
Toggle("Filter", isOn: $isOn)
.toggleStyle(.button)
.tint(.mint)
}
}
SwiftUI 3.0. Автоматический выбор изображения для TabView
В iOS 15 SwiftUI автоматически выбирает правильный вариант иконки из библиотеки SF Symbols при работе с TabView.
В соответствии с гайдлайнами iOS иконки в TabView должны быть заполнены. В тоже время в соответствии с гайдлайнами macOS они должны быть заштрихованы. Чтобы это хорошо работало на обеих платформах, теперь можно выбрать простую иконку, а SwiftUI определит стиль для этой иконки автоматически в зависимости от платформы. Так, например иконки для iOS будут заполненные, даже если мы этого не пропишем явно:
TabView {
Text("View 1")
.tabItem {
Label("Home", systemImage: "house")
}
Text("View 2")
.tabItem {
Label("Account", systemImage: "person")
}
Text("View 3")
.tabItem {
Label("Community", systemImage: "theatermasks")
}
}
SwiftUI 3.0. Основные действия для меню
В iOS 15 к меню может быть прикреплено действие по умолчанию, которое будет выполняться по нажатию на кнопку, а не по удержанию. То есть, что бы выполнить действие по умолчанию достаточно нажать один раз кнопку, а для того, что бы вызывать дополнительные пункты меню, нужно удерживать её:
TabView {
Text("View 1")
.tabItem {
Label("Home", systemImage: "house")
}
Text("View 2")
.tabItem {
Label("Account", systemImage: "person")
}
Text("View 3")
.tabItem {
Label("Community", systemImage: "theatermasks")
}
}
SwiftUI 3.0. Упрощенное закрытие представлений
До выхода текущего обновления программное закрытие представлений выполнялась следующим образом: presentationMode.wrappedValue.dismiss(). Теперь это делается проще:
@Environment(\.dismiss) var dismiss
Теперь для того, что бы заставить экран закрыться достаточно вызывать dismiss().
В туториале использованы материалы из:
- https://www.hackingwithswift.com/articles/235/whats-new-in-swiftui-for-ios-15
- https://www.hackingwithswift.com/quick-start/swiftui/how-to-dismiss-the-keyboard-for-a-textfield
- https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-list-or-a-foreach-from-a-binding
- https://www.hackingwithswift.com/quick-start/swiftui/how-to-find-which-data-change-is-causing-a-swiftui-view-to-update