Использование нескольких 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/
немного модифицированным под свои нужды.


#8

@haymob а вот так можно использовать?
Чтобы не хранить ссылку на window?

func presentInNewWindow(_ viewController: UIViewController, animated: Bool, completion: (() -> Void)?) {
        
        let window = UIWindow(frame: UIScreen.main.bounds).apply {
            $0.windowLevel = .alert + 1
            $0.rootViewController = UIViewController()
            $0.makeKeyAndVisible()
        }
        
        window.rootViewController?.present(viewController, animated: animated, completion: completion)
        objc_setAssociatedObject(viewController, "[\(arc4random())]", window, .OBJC_ASSOCIATION_RETAIN)
    }

Это для кастомных алертов - информативных контроллеров


#9

Использовать можно всё, каждый … как хочет.

Но вы пытаетесь обойти кривую архитектуру UIKit ещё более кривым костылём, и получаете цикл сильных ссылок window -> rootViewController -> presentedViewController -> window, так себе решение, нет гарантии что этот цикл разорвётся и window выгрузится из памяти.


#10

а разве он не выгрузится из памяти, после того как viewController выгрузится?
Я просто не придумал как можно отслеживать dissmiss контроллера чтобы явно очищать windows


#11

Выгрузится. Но не выгружается (сразу не пропадает), за счет того что создается цикл сильных ссылок.