Adapting your existing business solution for SwiftUI. Part 4. Navigation and configuration

Good day to all! With you I, Anna Zharkova, a leading mobile developer of Usetech.
Now let's talk about another interesting point in SwiftUI, navigation.

If you missed the previous articles in the series, you can read them here:

part 1
part 2
part 3

With the change in the description of the visual part and the transition to declarative syntax, the navigation control in the SwiftUI application has also changed. The use of the UIViewContoller is directly denied; the UINavigationController is not directly used. It is replaced by NavigationView.

@available(iOS 13.0, OSX 10.15, tvOS 13.0, *)
@available(watchOS, unavailable)
public struct NavigationView<Content> : View where Content : View {

    public init(@ViewBuilder content: () -> Content)

    //....
}

Essentially a wrapper over the UINavigationController and its functionality.


The main transition mechanism is NavigationLink (an analogue of segue), which is set immediately in the body View code.

public struct NavigationLink<Label, Destination> : View where Label : View, 
                                                                                           Destination : View {
.
    public init(destination: Destination, @ViewBuilder label: () -> Label)

    public init(destination: Destination, isActive: Binding<Bool>, 
                   @ViewBuilder label: () -> Label)

    public init<V>(destination: Destination, tag: V, selection: Binding<V?>,
                          @ViewBuilder label: () -> Label) where V : Hashable
//....
}

When creating a NavigationLink, it indicates the View to which the transition is made, as well as the View that the NavigationLink wraps, i.e. when interacting with it, the NavigationLink is activated. More information about the possible ways to initialize NavigationLink in the Apple documentation.

However, it should be borne in mind that there is no direct access to the View stack due to encapsulation, navigation is programmed only forward, returning is possible only 1 level back and then through the encapsulated code for the “Back” button



Also in SwiftUI there is no dynamic software navigation. If the transition is not tied to a trigger event, for example, pressing a button, but follows as a result of some kind of logic, then just do not do it. The transition to the next View is necessarily bound to the NavigationLink mechanism, which are set declaratively immediately when describing the View containing them. All.

If our screen should contain a transition to many different screens, then the code becomes cumbersome:

  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")
            }
}

We can manage links in several ways:
- NavigationLink activity control via the @Binding property

 NavigationLink(destination: ProfileView(), isActive: self.$isProfile) {
                Text("Profile")
            }

- control over link creation through a condition (state variables)

       if self.isProfile {
            NavigationLink(destination: ProfileView()) {
                Text("Profile")
            }
}

The first method adds to us the work of monitoring the state of control variables.
If we plan to navigate more than 1 level ahead, then this is a very difficult task.

In the case of the screen of the list of similar elements, everything looks compact:

 NavigationView{
        List(model.data) { item in
            NavigationLink(destination: NewsItemView(item:item)) {
            NewsItemRow(data: item)
            }
        }

The most serious problem of NavigationLink, in my opinion, is that all View links specified are not lazy. They are created not at the moment the link is triggered, but at the time of creation. If we have a list of many elements or transitions to many different View heavy content, then this does not affect the performance of our application in the best way. If we still have ViewModel attached to these View with logic in the implementation of which life-cycle View is not taken into account or not taken into account correctly, then the situation becomes very difficult.

For example, we have a news list with elements of the same type. We have never yet gone over to a single screen of a single news item, and the models already hang in our memory:



What can we do in this case to make our life easier?

First, recall that View does not exist in a vacuum, but is rendered in the UIHostingController.

open class UIHostingController<Content> : UIViewController where Content : View {

    public init(rootView: Content)

    public var rootView: Content
//...
}

And this is the UIViewController. So we can do the following. We will transfer all responsibility for the transition to the next View inside the new UIHostingController to the controller of the current View. Let's create navigation and configuration modules that we will call from our View.

The navigator working with the UIViewController will look like this:

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<Content:View>(screen: Content.Type, _ data: Any? = nil) {
     if let vc = ModuleConfig.shared.config(screen: screen)?
        .createScreen(data) {
        self.nc?.pushViewController(vc, animated: true)
        }
   }

By the same principle, we will create a factory of configurators, which will give us the implementation of the configurator of a specific module:

protocol IConfugator: class {
    func createScreen(_ data: Any?)->UIViewController
}

class ModuleConfig{
    private init(){}
    static let shared = ModuleConfig()
    
    func config<Content:View>(screen: Content.Type)->IConfugator? {
        if screen == NewsListView.self {
            return NewsListConfigurator.shared
        }
      // -
        return nil
    }
}

The navigator, by the type of screen, asks for the configurator of a particular module, transfers to it all the necessary information.

class NewsListConfigurator: IConfugator {
    static let shared = NewsListConfigurator()
    
    func createScreen(_ data: Any?) -> UIViewController {
         var view = NewsListView()
        let presenter = NewsListPresenter()
         let interactor = NewsListInteractor()
        
        interactor.output = presenter
        presenter.output = view
        view.output = interactor
        
        let vc = UIHostingController<ContainerView<NewsListView>>
                    (rootView: ContainerView(content: view))
        return vc
    }
}

The configurator gives away the UIViewController, which is the Navigator and pushes the UINavigationController onto the shared stack.



Replace NavigationLink in the code with a call to Navigator. As a trigger, we will have an event of clicking on a list item:

  List(model.data) { item in
            NewsItemRow(data: item)
                .onTapGesture {
                Navigator.shared.open(screen: NewsItemView.self, item)
            }
        }

Nothing prevents us from invoking Navigator in any View method in the same way. Not just inside the body.

Besides the fact that the code has become noticeably cleaner, we also unloaded the memory. After all, with this approach, the View will be created only when called.



Now our SwiftUI application is easier to expand and modify. The code is clean and beautiful.
You can find the example code here .

Next time we’ll talk about the deeper implementation of Combine.

All Articles