nicklockwood / layout

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

Nested Layout node troubles #160

Closed sidhenn closed 5 years ago

sidhenn commented 5 years ago

I am trying to do the following nested layouts with no luck. If I have outlets defined ONLY inside the VC1 class - I can reference the 'userName' property. Or if I have outlets defined for ONLY VC2 - I can reference the 'isTimerRunning' property.

But when I have VC2.xml called through VC1.xml the simulator throws the error:

Strangely - when I remove the outlet definitions in VC1, Layout can reference isTimerRunning again. I suspect its because I'm overriding the Layout Node.

Is there is a way to make this work?

Main.swift

class Main: UIViewController, LayoutLoading, UITabBarControllerDelegate
{
    ...
    override func viewDidLoad() {
        super.viewDidLoad()
        self.loadLayout(
            named: "Main.xml"
        )
    }
    ...
}

Main.xml

<UITabBarController>
    ...
    <UIViewController xml="VC1.xml" />
    ...
</UITabBarController>

VC1.xml

<VC1
    outlet="VC1Node">
    ...
    <UILabel text="{userName}" />
    ...
    <UIViewController xml="VC2.xml"/>
    ...
</VC1>

VC2.xml

<VC2
    outlet="VC2Node">
    ...
    <UIButton title="{isTimerRunning ? '| |':'>'}" />
    ...
</VC2>

VC1.swift

class VC1: UIViewController
{
    @IBOutlet var VC1Node: LayoutNode? {
        didSet {
            VC1Node?.setState([
                "userName": "My User Name"
                ])
        }
    }
    ...
}

VC2.swift

class VC2: UIViewController
{
    ...
    @IBOutlet var VC2Node: LayoutNode? {
        didSet {
            VC2Node?.setState([
                "isTimerRunning"    : true,
            ])
        }
    }
    ...
}
nicklockwood commented 5 years ago

@den2k try using template="..." instead of xml="..." to include your nested layouts.

sidhenn commented 5 years ago

I made the following change but the same error occurs. I'm still a bit baffled by what is happening. I'm not so experienced with swift or layout to know exactly where things are going wrong.

This is what I changed. Everything else stayed the same. Pretty sure I'm not using template in the way it is intended. Thank you.

VC2.xml

<VC1
    outlet="VC1Node">
    ...
    <UILabel text="{userName}" />
    ...
    <VC2 template="VC2.xml"/>
    ...
</VC1>
nicklockwood commented 5 years ago

@den2k I think it’s probably a race condition, where isTimerRunning is being evaluated before the @IBOutlet setter that sets its state is being called.

There are a few options for how to fix that, but I’ll need to recreate the project and try it out to see what works best.

I’ll try that and get back to you.

nicklockwood commented 5 years ago

@den2k OK, this seems to work. Add the following to your VC2 class:

    override class func create(with node: LayoutNode) throws -> UIViewController {
        node.setState([
            "isTimerRunning" : true,
        ])
        return try super.create(with: node)
    }

Basically this ensures that the state variable exists at the time of the initial creation of the VC.

You can still keep the VC2Node outlet didSet as well if you want, in which case the value for isTimerRunning inside the create method doesn't actually matter (as long as it's non-nil) because it will be immediately overridden by the value set in the outlet didSet.

nicklockwood commented 5 years ago

@den2k I've got a fix for the race condition. I'll push a new release with the fix - then your original code should work.

nicklockwood commented 5 years ago

@den2k OK, this should be fixed in the latest release.

sidhenn commented 5 years ago

Thank you. I just updated Layout to v0.6.33 and this has fixed the problem. I went back to the original code structure at the beginning of the post, and it works well! Thanks again!