HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>В предыдущих статьях мы поговорили об<a>адаптации уже существующего бизнес-приложения</a>под SwiftUI, рассмотрели<a>работу с готовыми библиотеками под UIKit</a>и разобрали<a>особенности архитектуры</a>. Остался еще один интересный момент:<strong>навигация</strong>.</p>
1 <p>В предыдущих статьях мы поговорили об<a>адаптации уже существующего бизнес-приложения</a>под SwiftUI, рассмотрели<a>работу с готовыми библиотеками под UIKit</a>и разобрали<a>особенности архитектуры</a>. Остался еще один интересный момент:<strong>навигация</strong>.</p>
2 <p>С изменением описания визуальной части и переходом к декларативному синтаксису изменилось и управление навигацией в приложении SwiftUI. Использование UIViewContoller напрямую отрицается, UINavigationController напрямую не используется. На смену ему приходит NavigationView.</p>
2 <p>С изменением описания визуальной части и переходом к декларативному синтаксису изменилось и управление навигацией в приложении SwiftUI. Использование UIViewContoller напрямую отрицается, UINavigationController напрямую не используется. На смену ему приходит NavigationView.</p>
3 @available(iOS 13.0, OSX 10.15, tvOS 13.0, *) @available(watchOS, unavailable) public struct NavigationView&lt;Content&gt; : View where Content : View { public init(@ViewBuilder content: () -&gt; Content) //.... }<p>По сути обертка над UINavigationController и его функционалом.</p>
3 @available(iOS 13.0, OSX 10.15, tvOS 13.0, *) @available(watchOS, unavailable) public struct NavigationView&lt;Content&gt; : View where Content : View { public init(@ViewBuilder content: () -&gt; Content) //.... }<p>По сути обертка над UINavigationController и его функционалом.</p>
4 <p>Основным механизмом перехода является NavigationLink (аналог segue), который задается сразу же в коде body View.</p>
4 <p>Основным механизмом перехода является NavigationLink (аналог segue), который задается сразу же в коде body View.</p>
5 public struct NavigationLink&lt;Label, Destination&gt; : View where Label : View, Destination : View { . public init(destination: Destination, @ViewBuilder label: () -&gt; Label) public init(destination: Destination, isActive: Binding&lt;Bool&gt;, @ViewBuilder label: () -&gt; Label) public init&lt;V&gt;(destination: Destination, tag: V, selection: Binding&lt;V?&gt;, @ViewBuilder label: () -&gt; Label) where V : Hashable //.... }<p>При создании NavigationLink указывает View, на который осуществляется переход, а также View, который NavigationLink оборачивает, т. е при взаимодействии с которым NavigationLink активизируется. Больше информации о возможных способах инициализации NavigationLink в документации Apple</p>
5 public struct NavigationLink&lt;Label, Destination&gt; : View where Label : View, Destination : View { . public init(destination: Destination, @ViewBuilder label: () -&gt; Label) public init(destination: Destination, isActive: Binding&lt;Bool&gt;, @ViewBuilder label: () -&gt; Label) public init&lt;V&gt;(destination: Destination, tag: V, selection: Binding&lt;V?&gt;, @ViewBuilder label: () -&gt; Label) where V : Hashable //.... }<p>При создании NavigationLink указывает View, на который осуществляется переход, а также View, который NavigationLink оборачивает, т. е при взаимодействии с которым NavigationLink активизируется. Больше информации о возможных способах инициализации NavigationLink в документации Apple</p>
6 <p>Однако стоит учитывать, что из-за инкапсуляции прямого доступа к стеку View нет, навигация программно задается только вперед, возврат возможен только на 1 уровень назад и то через инкапсулированный код для кнопки "Back".</p>
6 <p>Однако стоит учитывать, что из-за инкапсуляции прямого доступа к стеку View нет, навигация программно задается только вперед, возврат возможен только на 1 уровень назад и то через инкапсулированный код для кнопки "Back".</p>
7 <p>Также в SwiftUI нет динамической программной навигации. Если переход привязан не к триггерному-событию, например, нажатию на кнопку, а следует как результат какой-то логики, то просто так это не сделать. Переход на следующий View обязательно привязывается к механизму NavigationLink, которые задаются декларативно сразу же при описании содержащего их View. Все.</p>
7 <p>Также в SwiftUI нет динамической программной навигации. Если переход привязан не к триггерному-событию, например, нажатию на кнопку, а следует как результат какой-то логики, то просто так это не сделать. Переход на следующий View обязательно привязывается к механизму NavigationLink, которые задаются декларативно сразу же при описании содержащего их View. Все.</p>
8 <p>Если наш экран должен содержать переход на много разных экранов, то код становится громоздким:</p>
8 <p>Если наш экран должен содержать переход на много разных экранов, то код становится громоздким:</p>
9 NavigationView{ NavigationLink(destination: ProfileView(), isActive: self.$isProfile) { Text("Profile") } NavigationLink(destination: Settings(), isActive: self.$isSettings) { Text("Settings") } NavigationLink(destination: Favorite(), isActive: self.$isFavorite) { Text("Favorite") } NavigationLink(destination: Login(), isActive: self.$isLogin) { Text("Login") } NavigationLink(destination: Search(), isActive: self.$isSearch) { Text("Search") } }<p>Управлять ссылками мы можем несколькими способами: - управление активностью NavigationLink через @Binding-свойство:</p>
9 NavigationView{ NavigationLink(destination: ProfileView(), isActive: self.$isProfile) { Text("Profile") } NavigationLink(destination: Settings(), isActive: self.$isSettings) { Text("Settings") } NavigationLink(destination: Favorite(), isActive: self.$isFavorite) { Text("Favorite") } NavigationLink(destination: Login(), isActive: self.$isLogin) { Text("Login") } NavigationLink(destination: Search(), isActive: self.$isSearch) { Text("Search") } }<p>Управлять ссылками мы можем несколькими способами: - управление активностью NavigationLink через @Binding-свойство:</p>
10 NavigationLink(destination: ProfileView(), isActive: self.$isProfile) { Text("Profile") }<p>- управление созданием ссылки через условие (переменные состояния):</p>
10 NavigationLink(destination: ProfileView(), isActive: self.$isProfile) { Text("Profile") }<p>- управление созданием ссылки через условие (переменные состояния):</p>
11 if self.isProfile { NavigationLink(destination: ProfileView()) { Text("Profile") } }<p>Первый способ добавляет нам работы по контролю за состоянием управляющих переменных.</p>
11 if self.isProfile { NavigationLink(destination: ProfileView()) { Text("Profile") } }<p>Первый способ добавляет нам работы по контролю за состоянием управляющих переменных.</p>
12 <p>Если у нас планируется навигация на более, чем 1 уровень вперед, то это весьма тяжелая задача.</p>
12 <p>Если у нас планируется навигация на более, чем 1 уровень вперед, то это весьма тяжелая задача.</p>
13 <p>В случае экрана списка однотипных элементов все выглядит компактно:</p>
13 <p>В случае экрана списка однотипных элементов все выглядит компактно:</p>
14 NavigationView{ List(model.data) { item in NavigationLink(destination: NewsItemView(item:item)) { NewsItemRow(data: item) } }<p>Самой серьезной проблемой NavigationLink, на мой взгляд, является то, что все указываемые в ссылках View не lazy. Они создаются не в момент срабатывания ссылки, а в момент создания. Если у нас список на множество элементов или переходы на много разных тяжелых по контенту View, то это не лучшим образом сказывается на performance нашего приложения. Если же еще у нас к этим View привязаны ViewModel с логикой, в реализации которой не учтен или учтен не верно life-cycle View, то ситуация становится совсем тяжелой.</p>
14 NavigationView{ List(model.data) { item in NavigationLink(destination: NewsItemView(item:item)) { NewsItemRow(data: item) } }<p>Самой серьезной проблемой NavigationLink, на мой взгляд, является то, что все указываемые в ссылках View не lazy. Они создаются не в момент срабатывания ссылки, а в момент создания. Если у нас список на множество элементов или переходы на много разных тяжелых по контенту View, то это не лучшим образом сказывается на performance нашего приложения. Если же еще у нас к этим View привязаны ViewModel с логикой, в реализации которой не учтен или учтен не верно life-cycle View, то ситуация становится совсем тяжелой.</p>
15 <p>Например, у нас есть список новостей с однотипными элементами. Мы еще ни разу не перешли ни на один экран единичной новости, а модели уже висят в памяти:</p>
15 <p>Например, у нас есть список новостей с однотипными элементами. Мы еще ни разу не перешли ни на один экран единичной новости, а модели уже висят в памяти:</p>
16 <p>Что мы можем сделать в этом случае, чтобы облегчить себе жизнь?</p>
16 <p>Что мы можем сделать в этом случае, чтобы облегчить себе жизнь?</p>
17 <p>Во-первых, вспомним, что View существуют не в вакууме, а рендерятся в UIHostingController.</p>
17 <p>Во-первых, вспомним, что View существуют не в вакууме, а рендерятся в UIHostingController.</p>
18 open class UIHostingController&lt;Content&gt; : UIViewController where Content : View { public init(rootView: Content) public var rootView: Content //... }<p>А это UIViewController. Значит, мы можем сделать следующее. Мы перенесем всю ответственность за переход на следующий View внутри нового UIHostingController на контроллер текущего View. Создадим модули навигации и конфигурации, которые будем вызывать из нашего View.</p>
18 open class UIHostingController&lt;Content&gt; : UIViewController where Content : View { public init(rootView: Content) public var rootView: Content //... }<p>А это UIViewController. Значит, мы можем сделать следующее. Мы перенесем всю ответственность за переход на следующий View внутри нового UIHostingController на контроллер текущего View. Создадим модули навигации и конфигурации, которые будем вызывать из нашего View.</p>
19 <p>Навигатор, работающий с UIViewController, будет иметь такой вид:</p>
19 <p>Навигатор, работающий с UIViewController, будет иметь такой вид:</p>
20 class Navigator { private init(){} static let shared = Navigator() private weak var view: UIViewController? internal weak var nc: UINavigationController? func setup(view: UIViewController) { self.view = view } internal func open&lt;Content:View&gt;(screen: Content.Type, _ data: Any? = nil) { if let vc = ModuleConfig.shared.config(screen: screen)? .createScreen(data) { self.nc?.pushViewController(vc, animated: true) } }<p>По тому же принципу мы создадим фабрику конфигураторов, которая будет нам выдавать реализацию конфигуратора конкретного модуля:</p>
20 class Navigator { private init(){} static let shared = Navigator() private weak var view: UIViewController? internal weak var nc: UINavigationController? func setup(view: UIViewController) { self.view = view } internal func open&lt;Content:View&gt;(screen: Content.Type, _ data: Any? = nil) { if let vc = ModuleConfig.shared.config(screen: screen)? .createScreen(data) { self.nc?.pushViewController(vc, animated: true) } }<p>По тому же принципу мы создадим фабрику конфигураторов, которая будет нам выдавать реализацию конфигуратора конкретного модуля:</p>
21 protocol IConfugator: class { func createScreen(_ data: Any?)-&gt;UIViewController } class ModuleConfig{ private init(){} static let shared = ModuleConfig() func config&lt;Content:View&gt;(screen: Content.Type)-&gt;IConfugator? { if screen == NewsListView.self { return NewsListConfigurator.shared } //код какой-то return nil } }<p>Навигатор по типу экрана запрашивает конфигуратор конкретного модуля, передает ему всю необходимую информацию.</p>
21 protocol IConfugator: class { func createScreen(_ data: Any?)-&gt;UIViewController } class ModuleConfig{ private init(){} static let shared = ModuleConfig() func config&lt;Content:View&gt;(screen: Content.Type)-&gt;IConfugator? { if screen == NewsListView.self { return NewsListConfigurator.shared } //код какой-то return nil } }<p>Навигатор по типу экрана запрашивает конфигуратор конкретного модуля, передает ему всю необходимую информацию.</p>
22 class NewsListConfigurator: IConfugator { static let shared = NewsListConfigurator() func createScreen(_ data: Any?) -&gt; UIViewController { var view = NewsListView() let presenter = NewsListPresenter() let interactor = NewsListInteractor() interactor.output = presenter presenter.output = view view.output = interactor let vc = UIHostingController&lt;ContainerView&lt;NewsListView&gt;&gt; (rootView: ContainerView(content: view)) return vc } }<p>Конфигуратор отдает UIViewController, который Navigator и кладет в общий стек UINavigationController.</p>
22 class NewsListConfigurator: IConfugator { static let shared = NewsListConfigurator() func createScreen(_ data: Any?) -&gt; UIViewController { var view = NewsListView() let presenter = NewsListPresenter() let interactor = NewsListInteractor() interactor.output = presenter presenter.output = view view.output = interactor let vc = UIHostingController&lt;ContainerView&lt;NewsListView&gt;&gt; (rootView: ContainerView(content: view)) return vc } }<p>Конфигуратор отдает UIViewController, который Navigator и кладет в общий стек UINavigationController.</p>
23 <p>Заменим NavigationLink в коде на вызов Navigator. В качестве триггера у нас будет событие нажатия на элемент списка:</p>
23 <p>Заменим NavigationLink в коде на вызов Navigator. В качестве триггера у нас будет событие нажатия на элемент списка:</p>
24 List(model.data) { item in NewsItemRow(data: item) .onTapGesture { Navigator.shared.open(screen: NewsItemView.self, item) } }<p>Ничего не мешает нам таким же образом вызывать Navigator в любом методе View. Не только внутри body.</p>
24 List(model.data) { item in NewsItemRow(data: item) .onTapGesture { Navigator.shared.open(screen: NewsItemView.self, item) } }<p>Ничего не мешает нам таким же образом вызывать Navigator в любом методе View. Не только внутри body.</p>
25 <p>Кроме того, что код стал ощутимо чище, мы еще и разгрузили память. Ведь при таком подходе View создастся только при вызове.</p>
25 <p>Кроме того, что код стал ощутимо чище, мы еще и разгрузили память. Ведь при таком подходе View создастся только при вызове.</p>
26 <p>Теперь наше приложение SwiftUI проще расширять и модифицировать. Код чистый и красивый. Код примера вы найдете по<a>ссылке</a>.</p>
26 <p>Теперь наше приложение SwiftUI проще расширять и модифицировать. Код чистый и красивый. Код примера вы найдете по<a>ссылке</a>.</p>
27 <p>В следующий раз поговорим про более глубокое внедрение Combine.</p>
27 <p>В следующий раз поговорим про более глубокое внедрение Combine.</p>
28  
28