SwiftUI 3.0. Четвертая часть

28 сентября 2021

В этой публикации мы поговорим о новой обертке над свойством @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().

В туториале использованы материалы из:

Содержание