NSDictionaryOfVariableBindings swift equivalent?

IosSwift

Ios Problem Overview


The Apple documentation shows an unsettling blank space under the 'Creating a Dictionary' section of the UIKit reference here.

Has anyone found a replacement for the NSDictionaryOfVariableBindings macro, or are we expected to just write our own?

EDIT - According to this perhaps the right approach is to write a global function to handle this? Looks like complex macros are out entirely.

Ios Solutions


Solution 1 - Ios

According to Apple source code:

> NSDictionaryOfVariableBindings(v1, v2, v3) is equivalent to [NSDictionary dictionaryWithObjectsAndKeys:v1, @"v1", v2, @"v2", v3, @"v3", nil];

So in Swift you can do the same using:

let bindings = ["v1": v1, "v2": v2, "v3": v3]

Solution 2 - Ios

NSDictionaryOfVariableBindings is, as you say, a macro. There are no macros in Swift. So much for that.

Nonetheless, you can easily write a Swift function to assign string names to your views in a dictionary, and then pass that dictionary into constraintsWithVisualFormat. The difference is that, unlike Objective-C, Swift can't see your names for those views; you will have to let it make up some new names.

[To be clear, it isn't that your Objective-C code could see your variable names; it's that, at macro evaluation time, the preprocessor was operating on your source code as text and rewriting it — and so it could just use the text of your variable names both inside quotes (to make strings) and outside (to make values) to form a dictionary. But with Swift, there is no preprocessor.]

So, here's what I do:

func dictionaryOfNames(arr:UIView...) -> Dictionary<String,UIView> {
    var d = Dictionary<String,UIView>()
    for (ix,v) in arr.enumerate(){
        d["v\(ix+1)"] = v
    }
    return d
}

And you call it and use it like this:

    let d = dictionaryOfNames(myView, myOtherView, myFantasicView)
    myView.addConstraints(
        NSLayoutConstraint.constraintsWithVisualFormat(
            "H:|[v2]|", options: nil, metrics: nil, views: d)
    )

The catch is that it is up to you to realize that the name for myOtherView in your visual format string will be v2 (because it was second in the list passed in to dictionaryOfNames()). But I can live with that just to avoid the tedium of typing out the dictionary by hand every time.

Of course, you could equally have written more or less this same function in Objective-C. It's just that you didn't bother because the macro already existed!

Solution 3 - Ios

That functionality is based on macro expansion which is currently not supported in Swift.

I do not think there is any way to do something similar in Swift at the moment. I believe you cannot write your own replacement.

I'm afraid you'll have to manually unroll the dictionary definition, even if it means repeating each name twice.

Solution 4 - Ios

So I hacked something together which seems to work:

func dictionaryOfVariableBindings(container: Any, views:UIView...) -> Dictionary<String, UIView> {
    var d = Dictionary<String, UIView>()
    let mirror = Mirror(reflecting: container)
    let _ = mirror.children.compactMap {
        guard let name = $0.label, let view = $0.value as? UIView else { return }
        guard views.contains(view) else { return }
        d[name] = view
    }
    return d
}

Usage:

        let views = dictionaryOfVariableBindings(container: self, views: imageView)

Solution 5 - Ios

ObjC runtime to the rescue!

i created an alternate solution, but it only works if each of the views are instance variables of the same object.

func DictionaryOfInstanceVariables(container:AnyObject, objects: String ...) -> [String:AnyObject] {
    var views = [String:AnyObject]()
    for objectName in objects {
        guard let object = object_getIvar(container, class_getInstanceVariable(container.dynamicType, objectName)) else {
            assertionFailure("\(objectName) is not an ivar of: \(container)");
            continue
        }
        views[objectName] = object
    }
    return views
}

can be used like this:

class ViewController: UIViewController {
    
    var childA: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor.redColor()
        return view
    }()
    
    var childB: UIButton = {
        let view = UIButton()
        view.setTitle("asdf", forState: .Normal)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor.blueColor()
        return view
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.addSubview(childA)
        self.view.addSubview(childB)
        
        let views = DictionaryOfInstanceVariables(self, objects: "childA", "childB")
        self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[childA]|", options: [], metrics: nil, views: views))
        self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[childB]|", options: [], metrics: nil, views: views))
        self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[childA][childB(==childA)]|", options: [], metrics: nil, views: views))
        
    }
    
}

unfortunately you still have to type the variable name in as a string, but it will at least assert if there is a typo. this definitely won't work in all situations, but helpful nonetheless

Solution 6 - Ios

Based on https://stackoverflow.com/a/55086673/1058199 by cherpak-evgeny, this UIViewController extension assumes that the container is self, the current viewController instance.

extension UIViewController {
    
    // Alex Zavatone 06/04/2019
    // Using reflection, get the string name of the UIView properties passed in
    // to create a dictionary of ["viewPropertyName": viewPropertyObject…] like
    // Objective-C's NSDictionaryForVariableBindings.
    func dictionaryOfBindings(_ arrayOfViews:[UIView?]) -> Dictionary<String, UIView> {
        var bindings = Dictionary<String, UIView>()
        let viewMirror = Mirror(reflecting: self)
        let _ = viewMirror.children.compactMap {
            guard let name = $0.label, let view = $0.value as? UIView else { return }
            guard arrayOfViews.contains(view) else { return }
            bindings[name] = view
        }
        return bindings
    }
}

Use it like so from within your viewController:

let viewArray = [mySwitch, myField, mySpinner, aStepper, someView]
let constraintsDictionary = dictionaryOfBindings(viewArray)

Tested in Xcode 10.2.1 and Swift 4.2.

Many thanks to Cherpak Evgeny for writing it in the first place.

Solution 7 - Ios

Once you've stored all your views as properties, you could also use reflection like so:

extension ViewController {
    func views() -> Dictionary<String, AnyObject> {
        var views = dictionaryOfProperties()
        views.forEach {
            if !($1 is UIView) {
                views[$0] = nil
            }
        }
        return views
    }
}

extension NSObject {

    func dictionaryOfProperties() -> Dictionary<String, AnyObject> {
        var result = Dictionary<String, AnyObject>()
        let mirror = Mirror(reflecting: self)
        for case let(label?, value) in mirror.children {
            result[label] = value as? AnyObject
        }
        return result
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionStakenborgView Question on Stackoverflow
Solution 1 - IosjhibberdView Answer on Stackoverflow
Solution 2 - IosmattView Answer on Stackoverflow
Solution 3 - IosAnalog FileView Answer on Stackoverflow
Solution 4 - IosCherpak EvgenyView Answer on Stackoverflow
Solution 5 - IosCaseyView Answer on Stackoverflow
Solution 6 - IosAlex ZavatoneView Answer on Stackoverflow
Solution 7 - IoscrizzisView Answer on Stackoverflow