Сегодня мы выпускаем самое большое обновление для нашей библиотеки SwiftUINavigation с момента её первого выпуска год назад. В нём обеспечена поддержка новых API-интерфейсов iOS 16, исправлены ошибки некоторых навигационных инструментов Apple, улучшена поддержка оповещений и диалоговых окон подтверждения, а также улучшена документация.
Присоединяйтесь к нам для быстрого обзора новых функций и обязательно обновитесь до версии 0.4.0, чтобы получить доступ ко всему этому и многому другому:
Навигационные стеки.
Исправления ошибок навигации.
Оповещения и диалоговые окна подтверждения.
Начните сегодня.
Навигационные стеки
iOS 16 в значительной степени изменила способ работы со структурной навигацией, реализовав новое вью верхнего уровня, вью NavigationStack, новый модификатор вью, navigationDestination и новые инициализаторы в NavigationLink. Эти новые инструменты обеспечивают лучшее разделение между источником и конечной точкой навигации, а также позволяют лучше управлять глубокими стеками функции.
В нашей библиотеке появился новый инструмент, построенный на основе модификатора вью navigationDestination(isPresented:), который позволяет управлять навигацией из логической привязки( булевой цепочки). Этот инструмент устранил один из самых больших недостатков NavigationLink, связанный с трудностью использования во вью списка, так как детализация происходила только в том случае, если строка была видна в списке. Это означает, что было невозможно программно создать глубокую ссылку на экран, если строка в данный момент не видна.
Модификатор вью navigationDestination исправил этот недостаток, позволяя иметь единственное место для выражения навигации, не встраивая ее в каждую строку списка:
func navigationDestination<V>(
isPresented: Binding<Bool>,
destination: () -> V
) -> some View where V : View
Однако логическая цепочка слишком упрощена для использования в инструменте доменного моделирования. Что, если вы хотите управлять навигацией с помощью части опционального состояния и далее передавать это состояние конечному вью?
Вот почему наша библиотека поставляется с дополнительной перегрузкой, называемой navigationDestination(unwrapping:), которая может управлять навигацией от цепочки к опционалу:
public func navigationDestination<Value, Destination: View>(
unwrapping value: Binding<Value?>,
@ViewBuilder destination: (Binding<Value>) -> Destination
) -> some View {
Это упрощает получение списка данных, которые вы хотите развернуть при нажатии на строку:
struct UsersListView: View {
@State var users: [User]
@State var editingUser: User?
var body: some View {
List {
ForEach(self.users) { user in
Button("\(user.name)") { self.editingUser = user }
}
}
.navigationDestination(unwrapping: self.$editingUser) { $user in
EditUserView(user: $user)
}
}
}
Это прекрасно работает, если у вас есть только одна конечная точка. Но, если вам нужна поддержка нескольких точек, у вас может возникнуть соблазн хранить несколько частей опционального состояния. Однако это приводит к резкому увеличению количества недопустимых состояний, например, когда они не nill одновременно более одного. SwiftUI считает это ошибкой пользователя и может привести к тому, что интерфейс перестанет отвечать или даже выйдет из строя.
Вот почему наша библиотека поставляется с другой перегрузкой, называемой navigationDestination(unwrapping:case:), которая позволяет управлять несколькими пунктами назначения из одного положения:
public func navigationDestination<Enum, Case, Destination: View>(
unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder destination: (Binding<Case>) -> Destination
) -> some View {
Это дает возможность смоделировать все места назначения для функции как одно перечисление и один элемент опционального состояния, указывающий на это перечисление. Например, список со строками для пользователей и категории, для которых любое касание должно быть детализировано до соответствующего экрана редактирования:
struct UsersListView: View {
@State var categories: [Category]
@State var users: [User]
@State var destination: Destination?
enum Destination {
case edit(user: User)
case edit(category: Category)
}
var body: some View {
List {
Section(header: Text("Users")) {
ForEach(self.users) { user in
Button("\(user.name)") { self.destination = .edit(user: user) }
}
}
Section(header: Text("Categories")) {
ForEach(self.categories) { category in
Button("\(category.name)") { self.destination = .edit(category: user) }
}
}
}
.navigationDestination(
unwrapping: self.$destination,
case: /Destination.edit(user:)
) { $user in
EditUserView(user: $user)
}
.navigationDestination(
unwrapping: self.$destination,
case: /Destination.edit(category:)
) { $category in
EditCategoryView(user: $category)
}
}
}
Это происходит таким образом, что компилятор может доказать, что два места назначения никогда не активны одновременно. В итоге, пункты назначения создают перечисления, а случаи перечисления являются взаимоисключающими.
Исправления ошибок навигации
Модификатор вью navigationDestination(isPresented:), выпущенный в iOS 16 - мощный, и вышесказанное показывает, что мы можем создавать отличные API поверх него, однако в нем есть некоторые ошибки.
Если вы запустите свое приложение с уже измененным состоянием навигации, что означает, что вы должны перейти к пункту назначения, UI будет полностью сломан. Он не будет детализирован, и, что еще хуже, нажатие на кнопку для принудительной детализации не сработает.
Мы подали отзыв (и рекомендуем вам его продублировать!), и этот простой пример демонстрирует проблему:
struct UserView: View {
@State var isPresented = true
var body: some View {
Button("Go to destination") {
self.isPresented = true
}
.navigationDestination(isPresented: self.$isPresented) {
Text("Hello!")
}
}
}
Это довольно катастрофично. Если вы используете navigationDestination(isPresented:) в своем коде, вы просто не сможете поддерживать такие вещи, как глубокая ссылка URL или глубокая ссылка push-уведомлений.
Тем не менее, мы смогли исправить эту ошибку в наших API. Если вы используете их, то можете быть уверены, что диплинкинг будет работать правильно и не потеряет работоспособность на любом количестве уровней в глубину. Это также исправляет давнюю ошибку в iOS <16, которая печально известна тем, что не может делать глубокие ссылки более чем на 2 уровня в глубину.
Оповещения и диалоговые окна подтверждения
Наша SwiftUINavigation с самого начала поддерживала улучшенные API-интерфейсы оповещений и диалогов подтверждений с использованием опционалов и перечислений, но с выпуском 0.4.0 мы сделали их еще более мощными.
Библиотека теперь поставляет типы данных, которые позволяют вам описывать показ предупреждения или диалогового окна подтверждения таким образом, который более удобен для тестирования. Это позволяет хранить эти значения в ваших ObservableObject соответствиях, чтобы ваше тестирование могло «дотянуться» до любой логики.
Например, предположим, что у вас есть интерфейс с кнопкой, которая может удалить элемент списка, но только в том случае, если он не «заблокирован». Мы можем смоделировать это в нашем ObservableObject как опубликованное свойство AlertState, вместе с перечислением для описания любых действий, которые пользователь может предпринять в оповещении:
@Published var alert: AlertState<AlertAction>
enum AlertAction {
case confirmDeletion
}
Затем вы можете изменить это состояние в любое время, чтобы указать, что должно отображать предупреждение:
func deleteButtonTapped() {
if item.isLocked {
self.alert = AlertState {
TextState("Cannot be deleted")
} message: {
TextState("This item is locked, and so cannot be deleted.")
}
} else {
self.alert = AlertState {
TextState("Delete?")
} actions: {
ButtonState(role: .destructive, action: .confirmDeletion) {
TextState("Yes, delete")
}
ButtonState(role: cancel) {
TextState("Nevermind")
}
} message: {
TextState(#"Are you sure you want to delete "\(item.name)"?"#)
}
}
}
И последний шаг для слоя модели – реализовать метод, который обрабатывает нажатие кнопки оповещения:
func alertButtonTapped(_ action: AlertAction) {
switch action {
case .confirmDeletion:
self.inventory.remove(id: item.id)
}
}
Затем, чтобы вью отображало оповещение, когда состояние оповещения становится не nil, нам просто нужно использовать alert(unwrapping:) API, который поставляется с нашей библиотекой.
struct ItemView: View {
@ObservedObject var model: ItemModel
var body: some View {
Form {
…
}
.alert(unwrapping: self.$model.alert) { action in
self.model.alertButtonTapped(action)
}
}
}
Обратите внимание, что во вью нет логики для того, какое оповещение показывать. Вся логика, когда отображать оповещение и какая информация отображается (заголовок, сообщение, кнопки) перенесена в модель, а потому её очень легко тестировать.Для тестирования вы можете просто настроить любые части состояния оповещения, которые вы хотите. Например, если вы хотите убедиться, что сообщение оповещения соответствует вашим ожиданиям, можете просто использовать XCTAssertEqual:
let headphones = Item(…)
let model = ItemModel(item: headphones)
model.deleteButtonTapped()
XCTAssertEqual(
model.alert.message,
TextState(#"Are you sure you want to delete "Headphones"?"#)
)
Начните сегодня
Начните пользоваться всеми мощными инструментами моделирования предметной области, которые поставляются со Swift (перечисления и опции!), добавив SwiftUINavigation в свой проект уже сегодня!
Подписывайся на наши соцсети: Telegram / VKontakte
Вступай в открытый чат для iOS-разработчиков: t.me/swiftbook_chat
Смотри бесплатные уроки по iOS-разработке с нуля