7

In storyboard have a UIView, with a constraint for example "0.1" proportional height.

Imagine you have a similar constraint "0.1", on views in many different scenes.

Say you want to change the value 0.1 to 0.975.

You have to change it individually everywhere.

Is there some way to make a sort of "global value", using @IBInspectable and/or constraints?

So that you can change them all at once, and see the results all at once in storyboard.

(Of course, at run-time you could propagate these in code. But it's better to see it on storyboard.)


Note for example that Wain's solution below works perfectly: you can set the value once, and affect everywhere in the app. But unfortunately it only does that at run time, you don't see it on storyboard.

JAL
  • 41,701
  • 23
  • 172
  • 300
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Perhaps I'm misinterpreting your question, but couldn't you just constrain the heights of two container views relative to one another in a single view controller? Could you give a more specific example if I misunderstood? – William Anderson Jun 23 '16 at 19:33
  • I think I understand. Have you tried creating an `IBDesignable` view controller class with that constraint and using it as a base class for your `UIViews`? I'm also very interested in whether or not this is possible. – William Anderson Jun 23 '16 at 19:45
  • What does the error message say? – William Anderson Jun 23 '16 at 20:26
  • I've added a big bounty! I've cleaned up some of the older comments here guys. Let's hope to get some attention on it – Fattie Jul 01 '16 at 22:43
  • It's unfortunate there is no really good solution to this problem!! Maybe I will try to ask a more focussed question, perhaps other ideas will come. Thanks to all!! (I clicked the bounty to the highest voted answer, to avoid it just being wasted by the robot, cheers) Surely Apple should add this feature in the future, it seems so obvious. – Fattie Jul 07 '16 at 11:55

3 Answers3

4

I haven't tried it, and this is just typed in here so excuse typos, but I would consider creating an extension on NSLayoutConstraint, something along the lines of:

let constants = [
    "bannerHeight" : CGFloat(50)
]

extension NSLayoutConstraint {
func setCommonConstant(name: String) {
    self.constant = constants[name]!
}
}

and then using the user defined runtime attributes in Xcode so specify the name to use:

commonConstant, type String, value name

So, like this, on each Constraint anywhere in the storyboard:

enter image description here

Fattie
  • 27,874
  • 70
  • 431
  • 719
Wain
  • 118,658
  • 15
  • 128
  • 151
  • You mean because this is a runtime thing instead of a design time / in the storyboard thing ? – Wain Jun 30 '16 at 16:51
  • It does work, though there are a lot of typos in that code, improved it somewhat – Wain Jun 30 '16 at 18:02
  • Yes, that's correct. The other answer is interesting, perhaps both could be combined to add an inspectable to constraints so you can choose some preset. I don't see a way you can change a single setting in IB and have all constraints on disparate views automatically update though... – Wain Jul 02 '16 at 07:35
  • OK, I've finally got it! **This does work perfectly, thanks, I had no idea you could do that.** However sadly **unfortunately this does not update on storyboard .... *only at runtime!*** If only there was a way to make this work "instantly" like an IBInspectable!! – Fattie Jul 02 '16 at 13:54
  • Yeah, I can't see how you'd combine them and have it work on multiple items at the same time because design able is instance based. The only other way would be a subclass of constraint perhaps. Might be able to use that with designable though the constant would still need to be in code. – Wain Jul 02 '16 at 14:03
  • Right. Indeed, it would be no problem if the constant was actually in the code - you could just change it there. – Fattie Jul 02 '16 at 14:04
3

I can't think of a way to make IB update all "instances" at design time when a single instance has a property value change (And I think this would generally be undesired and non-standard behavior). I'm fairly certain it can't be done because I don't think IB keeps an instance of your custom view in memory for the duration of the IB session. I think what it does is instantiate the view, set properties on it, snapshot it for rendering, then releases it.

It should be possible to set up properties that set "default" values to be used by future instances of your view. We can achieve this by storing "default" values in NSUserDefaults (XCode's), and reading the default value inside initWithFrame:, which IB will call to initialize a new instance. We can gate all this default stuff using #if TARGET_INTERFACE_BUILDER so it doesn't show up at runtime on the device.

IB_DESIGNABLE
@interface CoolView : UIView

@property (strong, nonatomic) IBInspectable UIColor* frameColor;

#if TARGET_INTERFACE_BUILDER
@property (strong, nonatomic) IBInspectable UIColor* defaultFrameColor;
#endif

@end

@implementation CoolView

- (id) initWithFrame:(CGRect)frame
{
    self = [super initWithFrame: frame];
    if ( self != nil ) {

#if TARGET_INTERFACE_BUILDER
        NSData *colorData = [[NSUserDefaults standardUserDefaults] objectForKey: @"defaultCoolViewFrameColor"];
        if ( colorData != nil ) {
            self.frameColor = [NSKeyedUnarchiver unarchiveObjectWithData: colorData];;
        }
#endif

    }
    return self;
}

- (void) drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 10);
    CGRect f = CGRectInset(self.bounds, 10, 10);
    [self.frameColor set];
    UIRectFrame(f);
}

#if TARGET_INTERFACE_BUILDER
- (void) setDefaultFrameColor:(UIColor *)defaultFrameColor
{
    _defaultFrameColor = defaultFrameColor;

    NSData *colorData = [NSKeyedArchiver archivedDataWithRootObject: defaultFrameColor];
    [[NSUserDefaults standardUserDefaults] setObject:colorData forKey:@"defaultCoolViewFrameColor"];
}
#endif

@end

You could probably get closer to your original goal (updating all "instances" in IB) if you're okay with forcing IB to reload your xib/storyboard file altogether. To do this you'd likely have to use the technique above and extend it to include code in a custom initWithCoder: method on your view.

Possibly possibly you could get really tricky and try to "touch" the xib file that is being edited, and that might prompt IB to reload?

TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • Sophisticated. Will have to investigate this! – Fattie Jul 06 '16 at 11:55
  • *"I can't think of a way to make IB update all "instances" at design time"*... it's a good point. So, when you have ab `IBDesignable`, and you change the code (even trivially), it does indeed "re-storyboard-compile" the whole storyboard. So I think really, the only way to achieve what I'm saying is, indeed you have to have a value **in a code file**, and change that. (That's still highly convenient, much better than buggering around changing 100 IBInspectables or constraints.) – Fattie Jul 07 '16 at 01:35
2

I tried to find something similar when i started developing using Storyboard and Interface Builder. Unfortunately, xCode is not so scalable and code centralization using Interface Builder can't be implemented easily as can be done in Android development for example.

You could try something like this, use custom @IBDesignable attribute in UIView extension and change constraints values or adding constraint at runtime (but i don't know if it will affect Interface Builder too):

extension UIView {
    @IBInspectable var insertTopConstraint: Bool {
        set {
            // suppose 10 is your fixed value
            addConstraint(NSLayoutConstraint(item: self, attribute: .Top, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 10))
            updateConstraintsIfNeeded() // could be useful..
        }
        get {
            return true
        }
    }
}

It could be a starting point. Hope it helps

lubilis
  • 3,942
  • 4
  • 31
  • 54
  • It works for UI changes on view and they're visible in IB, for constraints i haven't tried and could make mistake. Otherwise you can maintain constraint value centralized using this approach. – lubilis Jun 23 '16 at 20:16
  • Inspectable borderColor was only an utility for me and i use it because i needed different colors. If you have to implement it for constraint you'll not have to change it in all places, you just set insertTopConstraint = true in IB and if you want to change constraint constant for all affected views you change it only in the inspectable code – lubilis Jun 23 '16 at 20:36
  • You seem to have the right idea here and I hope someone smarter than me can work it out from there !!! :) – Fattie Jun 30 '16 at 13:45