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

Эта статья ориентирована на SwiftUI на iOS / iPadOS 14, а пример кода написан с использованием Xcode 12.5. Код в этой статье теперь доступен на GitHub.

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

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

Модальные окна в SwiftUI

Одним из наиболее распространенных модальных шаблонов в SwiftUI является концепция листа. При активации ваш основной вид кажется немного отступает на задний план, а новый модальный вид скользит снизу вверх, прежде чем охватить весь вид (на iPhone; на iPad модальные листы занимают центр экрана.

Эти листы имеют встроенный метод взаимодействия; вы можете отменить, проведя по ним вниз на iOS или нажав за пределами их области на iPadOS. Но вы также можете добавить действия, чтобы закрыть лист самостоятельно.

Наиболее распространенный способ программирования листа - запуск его представления с помощью логической переменной @State, например:

struct ContentView: View {
  @State private var showModal = false

  var body: some View {
    Button("Click Me!") {
      showModal = true
    }
    .sheet(isPresented: $showModal) {
      ModalView()
    }
  }
}

Здесь нажатие кнопки изменяет состояние ContentView, представление реагирует на это изменение состояния, представляя ModalView, когда showModal имеет значение true. Отмена модального окна смахиванием или касанием работает за счет изменения showModal обратно на false - вот почему sheet требуется привязка ссылки к значению.

Отклонение модальных окон вручную

Но если мы добавляем наши собственные кнопки Отмена или Сохранить в модальное окно и хотим, чтобы лист исчезал при нажатии на них, мы обычно не получаем доступ к этой начальной showModal переменная напрямую.

Вместо этого среда нашего модального представления имеет свойство под названием режим презентации, которое сообщает нам, виден ли лист, и, что более важно, дает нам команду закрыть его:

struct ModalView: View {
  @Environment(\.presentationMode) var presentationMode

  var body: some View {
    // ...some view contents...
    Button("Cancel") {
      presentationMode.wrappedValue.dismiss()
    }
  }
}

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

Полезно отметить, что представление отклоняется не в режиме презентации. Режим представления имеет ссылку на эту связанную переменную, которую мы указали в isPresented, и меняет ее значение на false. И это вызывает исчезновение модального окна.

Таким образом, мы могли бы сделать это сами, передав ссылку на переменную непосредственно в представление:

// in ContentView
.sheet(isPresented: $showModal) {
  ModalView(isShowing: $showModal)
}

// in ModalView
struct ModalView: View {
  var isShowing: Binding<Bool>
  
  var body: some View {
    Button("Cancel") {
	  isShowing = false
    }
  }
}

Причина, по которой мы этого не делаем, как правило, заключается в том, что у модальных окон есть другие способы создания экземпляров на основе необязательных объектов - как только назначенный объект состояния не равен нулю, он будет отображать требуемое модальное окно. Если бы мы передавали ссылки, у нас была бы другая система управления состоянием внутри модального окна, в зависимости от того, как она вызывалась. Но каждая форма модального окна поддерживает режим презентации независимо от того, как он создан, поэтому большую часть времени мы можем написать код, который будет отклонен при вызове presentationMode.wrappedValue.dismiss(), и не беспокоиться о том, что делает код в вызывающем представлении.

Остановка отмены по инициативе пользователя

Даже без программирования листы всегда можно аннулировать.

А для случая использования, который я представил в верхней части этой статьи, где мы хотим представить логин, который должен быть завершен, прежде чем можно будет использовать остальную часть приложения, мы действительно не хотим, чтобы пользователь чтобы иметь возможность отменить его случайно. Таблицы не дают нам возможности изменить поведение по умолчанию.

К счастью для нас, у листов есть родственный тип просмотра, который называется полноэкранные обложки. Методы их вызова выглядят точно так же, за исключением того, что мы используем модификатор .fullScreenCover() вместо .screen():

struct ContentView: View {
  @State private var showModal = false

  var body: some View {
    Button("Click Me!") {
      showModal = true
    }
    .fullScreenCover(isPresented: $showModal) {
      ModalView()
    }
  }
}

Ключевое отличие состоит в том, что как на iOS, так и на iPadOS они полностью покрывают полезную площадь. Эта возможность отодвинуть его или нажать снаружи, чтобы отменить? Ушел. Единственный способ скрыть представление, вызываемое с помощью fullScreenCover, - это код.

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

Для этого отлично подойдет полноэкранное покрытие.

Образец приложения

NB: в macOS нет концепции полноэкранных обложек, поэтому следующий код предназначен только для iOS и iPadOS (он будет работать, если вы используете «Мой Mac (разработан для iPad). ) ”Место назначения сборки для создания приложения Catalyst). У меня есть пара заметок по поводу. родной macOS SwiftUI, который может быть интересен.

Код этого примера доступен на GitHub.



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

В Xcode создайте новое приложение iOS, используя стандартный шаблон.

1. Аутентификатор

К основному проекту добавьте новый файл Swift, Authenticator.swift. Это инкапсулирует весь наш фальшивый код входа в систему.

class Authenticator: ObservableObject {
  @Published var needsAuthentication: Bool
  @Published var isAuthenticating: Bool

  init() {
    self.needsAuthentication = true
    self.isAuthenticating = false
  }

  func login(username: String, password: String) {
    self.isAuthenticating = true
    // emulate a short delay when authenticating
    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
      self.isAuthenticating = false
      self.needsAuthentication = false
    }
  }

  func logout() {
    self.needsAuthentication = true
  }
}

Это содержит несколько деталей:

  1. needsAuthentication сообщает нам, когда нам нужно отобразить экран входа в систему, поэтому он будет истинным, когда мы выходим из системы, и ложным, когда мы вошли в систему.
  2. isAuthenticating - это флаг, который мы устанавливаем, когда делаем вид, что запрашиваем удаленный сервер, позволяя форме входа выглядеть так, как будто она что-то делает
  3. login(username:password:) принимает любые два значения, но ничего с ними не делает. Вместо этого он просто ждет пару секунд, а затем сбрасывает наши опубликованные переменные экземпляра. Обратите внимание, что это делается в основной очереди отправки - все, что может вызвать перерисовку пользовательского интерфейса, должно происходить в основном потоке, включая изменение состояния нашего приложения.
  4. logout меняет значение needsAuthentication обратно на true, что означает, что нам нужно снова увидеть логин.

2. Добавьте аутентификатор в файл приложения.

Затем обновите файл ‹AppName› App.swift, чтобы создать экземпляр нашего объекта аутентификатора и передать его во все представления нашего приложения как объект среды:

import SwiftUI

@main
struct AuthDemoApp: App {
  @StateObject var authenticator = Authenticator()

  var body: some Scene {
    WindowGroup {
      RootView()
        .environmentObject(authenticator)
    }
  }
}

3. Создайте корневое представление.

RootView - это новый файл, который решает, показывать ли пользователю логин или просмотр содержимого. Мы добавим это сейчас:

import SwiftUI

struct RootView: View {
  @EnvironmentObject var authenticator: Authenticator

  var body: some View {
    ContentView()
      .fullScreenCover(isPresented: $authenticator.needsAuthentication) {
        LoginView()
          .environmentObject(authenticator) // see note
      }
  }
}

struct RootView_Previews: PreviewProvider {
  static var previews: some View {
    RootView()
      .environmentObject(Authenticator())
  }
}

Объявление @EnvironmentObject означает, что мы получаем доступ к тому же экземпляру Authenticator, который мы создали в файле приложения. Мы используем его свойство needsAuthentication для управления модальным представлением: повторюсь, это значение истинно, когда мы выходим из системы, и в этом случае будет отображаться LoginView (которое мы создадим дальше).

В предварительном просмотре мы можем создать новый экземпляр Authenticator только для предварительного просмотра. Сейчас это нормально, поскольку Authenticator - поддельный сервис; если вы создаете что-то с возможностью подключения к Интернету в реальном мире, вы можете ограничить доступ к нему, однако это выходит за рамки данной статьи.

Обратите внимание, что мы вручную добавляем версию среды нашего аутентификатора, которая была передана нам в LoginView() здесь, в отмеченной строке выше. Поскольку SwiftUI был впервые представлен в iOS 13, значения среды - особенно пользовательские, которые мы определяем как объекты среды - не были автоматически переданы модальным представлениям, поскольку эти представления, как правило, существуют в отдельной иерархии от основного представления, которое их вызывает.

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

4. Создайте представление входа в систему.

Пришло время создать нашу форму входа! Я не собираюсь здесь ехать в город по стилю, так как это не имеет отношения к пониманию того, как работают модальные окна. Но я сделаю вид входа в систему ярким цветом фона, чтобы выделить его по сравнению с содержимым, когда вы вошли в систему. Тем не менее, это делает цвет кнопки по умолчанию не таким контрастным, как хотелось бы для рабочего приложения.

//
//  LoginView.swift
//  AuthDemo
//
//  Created by Scott Matthewman on 22/05/2021.
//

import SwiftUI

struct LoginView: View {
  @EnvironmentObject var authenticator: Authenticator

  @State private var userName: String = ""
  @State private var password: String = ""

  var body: some View {
    ZStack {
      Color.yellow
        .ignoresSafeArea(.all)
      VStack {
        Text("Please log in")
          .font(.title2)
        TextField("User name", text: $userName)
          .textFieldStyle(RoundedBorderTextFieldStyle())
          .autocapitalization(.none)
          .disableAutocorrection(true)
        SecureField("Password", text: $password)
          .textFieldStyle(RoundedBorderTextFieldStyle())
        Button(authenticator.isAuthenticating ? "Please wait" : "Log in"
) {
          authenticator.login(username: userName, password: password)
        }
        .disabled(isLoginDisabled)
        ProgressView()
          .progressViewStyle(CircularProgressViewStyle())
          .opacity(authenticator.isAuthenticating ? 1.0 : 0.0)
      }
      .frame(maxWidth: 320)
      .padding(.horizontal)
    }
  }

  private var isLoginDisabled: Bool {
    authenticator.isAuthenticating || userName.isEmpty || password.isEmpty
  }
}

struct LoginView_Previews: PreviewProvider {
  static var previews: some View {
    LoginView()
      .environmentObject(Authenticator())
  }
}

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

  • в поле имени пользователя и пароля добавлен текстовый стиль с закругленными краями, который также делает их фон белым, чтобы выделяться на фоне всего экрана.
  • Для имени пользователя отключены как автоматическое использование заглавных букв, так и автокоррекция. Значения по умолчанию действительно полезны для многих типов ввода текста, но каждый раз, когда вы ищете данные в определенном формате, вы должны подумать о том, что лучше всего соответствует тому, что пользователь ожидает ввести. Для электронных писем, URL-адресов, дескрипторов Twitter и т. Д. Установка .keyboardType также была бы невероятно полезной.
  • Кнопка входа в систему неактивна, если имя пользователя или пароль пусты или если мы находимся в процессе аутентификации.
  • Мы также показываем круговое представление хода выполнения во время аутентификации, используя его непрозрачность, чтобы показать или скрыть его, чтобы форма всегда оставляла для нее место и не прыгала по странице, когда она отображается и скрывается.
  • Установка максимальной ширины VStack вокруг элементов входа сохраняет их разумную ширину на iPad или iPhone с альбомной ориентацией. Помните, что полноэкранные обложки могут покрывать весь экран (подсказка в названии) - и без ограничений текстовые поля будут перемещаться из стороны в сторону.

Кнопка входа в систему вызывает метод login(username:password:) в нашем аутентификаторе, который, если вы помните, игнорирует любые передаваемые ему значения и вводит 2-секундную задержку для имитации связи с каким-либо сервером.

Осталось только одно - разрешить пользователю выйти из системы.

5. Обновите ContentView.

У нашего объекта аутентификатора уже есть метод выхода из системы, поэтому давайте добавим его к новой кнопке в нашем существующем ContentView:

struct ContentView: View {
  @EnvironmentObject var authenticator: Authenticator

    var body: some View {
      VStack {
        Text("Hello, world!")
          .font(.title)
          .padding(.bottom)
        Button("Logout") {
          authenticator.logout()
        }
      }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
          .environmentObject(Authenticator())
    }
}

Вот и все. Создайте и запустите приложение, и вы должны увидеть форму входа, когда она впервые запускается, а затем исчезнет, ​​когда вы заполните форму. Выйдите из системы, и форма вернется:

Обратите внимание, что в этом случае нам нигде не нужно использовать presentationMode, чтобы закрыть модальное представление. Это потому, что его отображение привязано к $authenticator.needsAuthentication, для которого установлено значение false внутри метода login, когда вход был успешным. Итак, здесь ответственность за отображение формы входа в систему лежит на нашем Authenticator объекте, а RootView реагирует на изменение, показывая или скрывая форму при изменении свойства объекта.

Заворачивать

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

Просто помните, что полные экраны нужно исключать в коде, иначе вы навсегда оставите своих пользователей застрять на закрывающем просмотре!

PS: для macOS

Как отмечалось выше, если вы создаете приложение для Mac с помощью Catalyst - версии iOS, специально созданной для работы на Mac, - приведенный выше код должен работать нормально.

Однако при сборке собственного Mac .fullScreenCover не существует - ваше приложение не скомпилируется, даже если вы попытаетесь это сделать.

Хорошая новость заключается в том, что .sheet модальные окна в macOS не имеют той же функции автоматической отмены, что и их однофамильцы в iOS, поэтому вы можете применить там те же принципы. Есть и другие вещи, которые следует учитывать, например, что делать, если ваше приложение Mac запускает несколько окон (или действительно, как ваше приложение iPad обрабатывает режим разделенного экрана), но это выходит за рамки этой статьи.

дальнейшее чтение