Использование нескольких UIWnidow

swift
ios

#1

Часто нужно перекрыть основной интерфейс:

  1. Показать алерт с ошибкой поверх всего.
  2. Показать индикатор загрузки поверх всего.
  3. Показать экран загрузки после старта приложения.
  4. Показать экран входа если пользователь не авторизован.

До недавнего времени я считал основными два способа:

  1. Переопределить рутконтроллер основного окна.
  2. Добавить сабвью к основному окну.

Первый способ это прям «лучшая практика» у индусов с медиума, второй тоже практикуется, и оба выглядят как костыли.

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

Для этого нужно:

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var loginWindow: UIWindow? // 1
    
    var loggedIn = false
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        if !loggedIn {
            let loginViewController = UIViewController()
            loginViewController.view.backgroundColor = .red
            
            loginWindow = UIWindow(frame: UIScreen.main.bounds) // 2
            loginWindow!.rootViewController = loginViewController
            loginWindow!.windowLevel = .normal + 1 // 3
            loginWindow!.makeKeyAndVisible() // 4
        }
        return true
    }
}
  1. Создать ещё одну переменную с UIWindow.
  2. Создать окно.
  3. Присвоить windowLevel, это z-index окна, normal + 1 будет выше главного окна и будет виден статус бар.
  4. Показывает и делает ключевым это окно (isHidden = false, isKeyWindow = true).

Если запустить дебаггер можно увидеть два окна и иерархии:
img1

Алерт показать/скрыть можно так:

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var alertWindow: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            self!.alertWindow = UIWindow(frame: UIScreen.main.bounds)
            self!.alertWindow!.rootViewController = UIViewController()
            self!.alertWindow!.windowLevel = .alert + 1 // 1
            self!.alertWindow!.makeKeyAndVisible()
            
            let alert = UIAlertController(title: "Hello1", message: nil, preferredStyle: .alert)
            self!.alertWindow!.rootViewController!.present(alert, animated: true)
            
            alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
                self!.alertWindow?.isHidden = true // 2
                self!.alertWindow = nil // 3
            })
        }
        
        return true
    }
}
  1. Alert + 1 выше всех, даже статус бар перекрывает.
  2. Скрывает окно и передаёт статус ключевого, ближайшему окну в иерархии.
  3. При обнулении ссылки происходит удаление окна.

Иерархия выглядет так:


#2

Сомнительные аргументы.


#3

Конечно это моё личное мнение, для кого-то это лучшая практика.


#4

Почему сомнительные? Это лучше чем делать расширение на делегат, чтобы выковырнуть из презентейшен слоя текущий контроллер, чтобы по иерархии не сломать показ модального контроллера.

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


#5

Я тоже вдоволь наигрался с иерархией вьюконтроллеров, прежде чем додумался до нового окна :slight_smile:


#6

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


#7

Я же не про сам способ реализации, а именно про аргументы.
Сам способ однозначно хороший, прост в реализации.
Лично я сам пользуюсь таким методом: https://swiftbook.ru/post/tutorials/ios-root-controller-navigation/
немного модифицированным под свои нужды.