Search code examples
iosinterface-builderibdesignableibinspectable

Making UIImage redraw on new position on @IB_Designable


I am creating this IB_Designable class. It is like a slider. See picture. Both elements are created with little stretchable UIImages.

enter image description here

I have this red square that is 66x66 pt. This square has to slide in X inside the gray rectangle.

I have create this class:

HEADER

#import <UIKit/UIKit.h>

IB_DESIGNABLE

@interface MyClass : UIView

// minimum and maximum slider value
@property (assign, nonatomic) IBInspectable CGFloat minimumValue;
@property (assign, nonatomic) IBInspectable CGFloat maximumValue;

@property (assign, nonatomic) IBInspectable CGFloat value;

@end

IMPLEMENTATION

#import "MyClass.h"

@interface MyClass() {
  __weak IBOutlet UIView *topContainer;
  CGFloat minimumThumbCoordinate;
  CGFloat maximumThumbCoordinate;
}

@property (weak, nonatomic) IBOutlet UIImageView *thumb;

@end


@implementation MyClass



- (void)awakeFromNib {
  [super awakeFromNib];

  // topContainer contains everything
  CGRect topContainerBounds = [topContainer bounds];
  CGRect thumbBounds = [self.thumb bounds];

  CGFloat topContainerWidth = CGRectGetWidth(topContainerBounds);
  CGFloat thumbWidth = CGRectGetWidth(thumbBounds);

  minimumThumbCoordinate = floorf(thumbWidth / 2.0f);
  maximumThumbCoordinate = floorf(topContainerWidth - minimumThumbCoordinate);

}

-(void)setValue:(CGFloat)value {

  if ((value < self.minimumValue) || (value > self.maximumValue)) return;

  // normalize values

  CGFloat minimumValueNormalized = self.minimumValue;
  CGFloat maximumValueNormalized = self.maximumValue;
  CGFloat desiredValue = value;

  if ((minimumValueNormalized < 0) && (maximumValueNormalized > 0)) {
    CGFloat absoluteMinimum = fabsf(self.minimumValue);
    minimumValueNormalized = 0;
    maximumValueNormalized += absoluteMinimum;
    desiredValue += absoluteMinimum;
  }

  CGFloat percentage = desiredValue/maximumValueNormalized;

  // find coordinate
  CGFloat coordinateRange = maximumThumbCoordinate - minimumThumbCoordinate;
CGFloat relativeCoordinate = percentage * coordinateRange;

CGPoint center = CGPointMake(relativeCoordinate, self.thumb.center.y);

  [self.thumb setCenter: center];

}

The problem is that the setValue method does not make the thumb move when I set the value on interface builder... any ideas?


Solution

  • You cannot have components of your thumbnail added in your storyboard. In this case, I suspect your thumb image view outlet is nil while it's being previewed in IB, and thus changing the value it is likely not updating any subview.

    You generally get around this by adding the subviews programmatically, e.g. something like:

    //  MyClass.h
    
    #import <UIKit/UIKit.h>
    
    IB_DESIGNABLE
    @interface MyClass : UIView
    
    @property (nonatomic, strong) IBInspectable UIImage *thumbnailImage;
    
    @property (nonatomic) IBInspectable CGFloat minimumValue;
    @property (nonatomic) IBInspectable CGFloat maximumValue;
    
    @property (nonatomic) IBInspectable CGFloat value;
    @property (nonatomic, readonly) CGFloat percent;
    
    @end
    

    And

    //  MyClass.m
    
    #import "MyClass.h"
    
    @interface MyClass ()
    
    @property (weak, nonatomic) UIImageView *thumbnailView;
    @property (nonatomic) CGFloat thumbWidth;
    
    @end
    
    @implementation MyClass
    
    - (instancetype)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self configureView];
        }
        return self;
    }
    
    - (instancetype)initWithCoder:(NSCoder *)coder {
        self = [super initWithCoder:coder];
        if (self) {
            [self configureView];
        }
        return self;
    }
    
    - (void)configureView {
        UIImageView *thumb = [[UIImageView alloc] initWithImage:self.thumbnailImage];
        [self addSubview:thumb];
        thumb.backgroundColor = [UIColor lightGrayColor]; // in case no image has been set
        thumb.clipsToBounds = true;                       // in case you change your `contentMode`
        thumb.contentMode = UIViewContentModeScaleToFill;
        _thumbnailView = thumb;
    
        _thumbWidth = 44;                                 // maybe you have another IBInspectable property for this...
    
        [self layoutThumbnail];
    }
    
    - (void)setThumbnailImage:(UIImage *)image {
        self.thumbnailView.image = image;
    }
    
    - (CGFloat)percent {
        CGFloat range = self.maximumValue - self.minimumValue;
        return range ? (self.value - self.minimumValue) / range : 0;
    }
    
    - (void)layoutSubviews {
        [super layoutSubviews];
    
        [self layoutThumbnail];
    }
    
    - (void)layoutThumbnail {
        CGFloat minX = self.thumbWidth / 2;
        CGFloat maxX = self.bounds.size.width - self.thumbWidth / 2;
    
        CGRect frame = CGRectMake(self.percent * (maxX - minX) + minX - self.thumbWidth / 2, 0, self.thumbWidth, self.bounds.size.height);
    
        self.thumbnailView.frame = frame;
    }
    
    - (void)setValue:(CGFloat)value {
        if (value < self.minimumValue)
            _value = self.minimumValue;
        else if (value > self.maximumValue) {
            _value = self.maximumValue;
        } else {
            _value = value;
        }
    
        [self layoutThumbnail];
    }
    
    @end
    

    Note, I update the thumbnail frame not only when you change the value, but also upon layoutSubviews. You want to make sure that the thumbnail frame updates if your control changes size at runtime (e.g. user rotates the device and the control changes size). I also made the thumbnail image a property so you could set this in IB, too, if you wanted. But maybe you have some hardcoded/default image that you use. I also thought that the percent that we use for laying out the thumbnail might potentially be useful by the code that used that control, so I exposed that property.

    Now, you're using image views, but if all you wanted were rounded corners of the main control and the thumbnail, I wouldn't use images, but rather just round the corners of the layer for the appropriate UIView objects. Also, I was unclear about your intent of certain things (e.g., I'm not sure what your topContainer is referring to ... you wouldn't generally reference views outside the view hierarchy of the designable view). And you reference the top level view being an image view, which you'd do if you wanted a background image there, too.

    But those are details that aren't relevant to your broader question. Hopefully this illustrates the idea, that if you create the subviews programmatically, you can see the thumbnail move as you change your IBInspectable value.


    I have seen some implementations here where people wanted to define all of the view hierarchy of the designable view with outlets. But in that case, you have a self-contained NIB for all of these subviews. If you search StackOverflow for "IBDesignable NIB", you'll see links to related answers.