nicklockwood / layout

A declarative UI framework for iOS
MIT License
2.23k stars 97 forks source link

What is the proper way to initialize a layout-based viewcontroller in another layout-based viewcontroller? #162

Closed wpcfan closed 5 years ago

wpcfan commented 5 years ago

I have a similar setup with the Sample App, i.e. I have a tabviewcontroller and 3 tabs: Home, Social and Me. In the MeViewController, I want to launch a drawer-like viewcontroller (which is also layout-based) when clicking the left nav button, but I ran into an exception, so I would like to know what is the recommended way to do this

image

The Layout xml is

<UITabBarController view.backgroundColor="white">
    <UITabBar
        backgroundColor="white"
        tintColor="accent"
    />
    <UINavigationController>
        <UINavigationBar
            backgroundColor="clear"
            backgroundImage="emptyImage"
            barStyle="black"
            barTintColor="primaryDark"
            isTranslucent="true"
            shadowImage="emptyImage"
            tintColor="textIcon"
        />

        <!-- Home -->
        <UIViewController
            navigationItem.rightBarButtonItem.action="scanInModalAction"
            navigationItem.rightBarButtonItem.image="iconAdd"
            navigationItem.title="{strings.main.navigation.home.title}"
            navigationItem.titleView="{homeTitleView}"
            tabBarItem.image="{iconTabHome}"
            title="{strings.home.tabbar.tab_home}"
            xml="HomeViewController.xml"
        />
    </UINavigationController>
    <UINavigationController>
        <UINavigationBar
            barStyle="black"
            barTintColor="primaryDark"
            tintColor="textIcon"
            titleColor="textIcon"
        />

        <!-- Pages -->
        <UIViewController
            navigationItem.title="{strings.main.navigation.social.title}"
            tabBarItem.image="{iconTabSocial}"
            title="{strings.home.tabbar.tab_social}"
            xml="SocialViewController.xml"
        />
    </UINavigationController>
    <UINavigationController>
        <UINavigationBar
            backgroundColor="clear"
            backgroundImage="emptyImage"
            barStyle="black"
            barTintColor="primaryDark"
            isTranslucent="true"
            shadowImage="emptyImage"
            tintColor="textIcon"
        />

        <!-- Text -->
        <UIViewController
            navigationItem.leftBarButtonItem.action="toggle"
            navigationItem.leftBarButtonItem.image="iconMenu"
            navigationItem.rightBarButtonItem.image="iconSettings"
            navigationItem.rightBarButtonItem.action="navigateToSettings"
            navigationItem.title="{strings.main.navigation.my.title}"
            tabBarItem.image="{iconTabMy}"
            title="{strings.home.tabbar.tab_me}"
            xml="MeViewController.xml"
        />
    </UINavigationController>
</UITabBarController>

The MeViewController is as follows

//
//  MyViewController.swift
//  Example
//

import URLNavigator
import Layout

private let images = [
    UIImage(named: "Scan"),
    UIImage(named: "Task"),
]

class MeViewController: BaseViewController {
    var leftMenu: UIViewController?
    var leftDrawerTransition: DrawerTransition?

    private let navigator = container.resolve(NavigatorType.self)!
    @objc var layoutNode: LayoutNode? {
        didSet {
            leftMenu = SideBarViewController()
            leftDrawerTransition = DrawerTransition(target: self, drawer: leftMenu)
        }
    }

    @objc var collectionView: UICollectionView? {
        didSet {
            collectionView?.registerLayout(
                named: "CollectionCell.xml",
                forCellReuseIdentifier: "standaloneCell"
            )
            collectionView?.registerLayout(
                named: "MeHeaderCell.xml",
                forCellReuseIdentifier: "meHeaderCell"
            )
        }
    }

    @objc func navigateToSettings() {
        self.navigator.push("example://me/settings")
    }
}

extension MeViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 500
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let identifier = (indexPath.row % 2 == 0) ? "meHeaderCell" : "standaloneCell"
        let node = collectionView.dequeueReusableCellNode(withIdentifier: identifier, for: indexPath)
        let image = images[indexPath.row % images.count]!

        node.setState([
            "row": indexPath.row,
            "image": image,
            "whiteImage": image.withRenderingMode(.alwaysOriginal),
            ])

        return node.view as! UICollectionViewCell
    }
}

extension MeViewController: UICollectionViewDelegate {

}

extension MeViewController: DrawerTransitionDelegate {
    @objc func toggle()  {
        if (self.leftDrawerTransition!.hasDismissView) {
            self.leftDrawerTransition!.presentDrawerViewController(animated: true)
        } else {
            self.leftDrawerTransition!.dismissDrawerViewController(animated: true)
        }
    }
}

The SidebarViewController is as follows

//
//  SideBarViewController.swift
//  Example
//
import URLNavigator
import Layout

class SideBarViewController: BaseViewController {
    private let CELL_REUSE_IDENTIFIER = "sidebarCell"
    private let navigator = container.resolve(NavigatorType.self)!
    @objc var tableView: UITableView? {
        didSet {
            guard let tableView = tableView else { return }
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: CELL_REUSE_IDENTIFIER)

        }
    }
}

extension SideBarViewController: UITableViewDataSource {
    // Section Header Title
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return nil
    }
    // rows in section
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 20
    }
    // dequeue cell
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CELL_REUSE_IDENTIFIER,   for: indexPath as IndexPath)
        cell.textLabel?.text = "Article \(indexPath.row)"
        return cell
    }
}

extension SideBarViewController: LayoutLoading {
    override func viewDidLoad() {
        super.viewDidLoad()

        self.loadLayout(named: "SideBarViewController.xml")
    }
}
nicklockwood commented 5 years ago

@wpcfan can you include your SideBarViewController.xml file?

wpcfan commented 5 years ago

@nicklockwood , the xml is as follows, just a very simple tableview

<Example.SideBarViewController>
    <UITableView
        backgroundColor="clear"
        contentInset.bottom="safeAreaInsets.bottom"
        contentInset.top="parent.top"
        contentInsetAdjustmentBehavior="never"
        contentOffset.y="0"
        estimatedSectionHeaderHeight="0"
        outlet="tableView"
        scrollIndicatorInsets.bottom="safeAreaInsets.bottom"
        scrollIndicatorInsets.top="parent.top"
        style="plain"
        top="parent.top">
        <!-- Table cell template -->
        <UITableViewCell
            detailTextLabel.highlightedTextColor="#fff7"
            detailTextLabel.text="This cell is defined as a template in Table.xml"
            detailTextLabel.textColor="#aaa"
            height="auto + 20"
            reuseIdentifier="sidebarCell"
            selectedBackgroundView.backgroundColor="tintColor"
            style="subtitle"
            textLabel.highlightedTextColor="white"
            textLabel.text="Title #{row}">

        </UITableViewCell>

    </UITableView>
</Example.SideBarViewController>
wpcfan commented 5 years ago

So I figure it out, If I change Example.SideBarViewController to UIViewController, all problems gone. But I don't understand, so when to use UIViewController instead of a specific controller name?

nicklockwood commented 5 years ago

@wpcfan it should work if you just remove the outer view controller from your XML and leave the tableView.

You cannot load a class from inside itself, so what was happening is that the SideBarViewController was loading a new instance of SideBarViewController from the xml and then trying to add it as a child to itself.

When you changed it to a regular UIViewController, it started adding that UIVC as a child controller instead, which fixes the crash, but you probably just want to add the TableView directly as a child view of ExampleViewController instead, without an intermediate view controller.

wpcfan commented 5 years ago

@nicklockwood thanks for detailed clarification, very helpful. closing this issue