9

I am building an Watch app where I want to overlay WKInterfaceImage with a group with a bunch of WKInterfaceLabel objects. Can't seem to be able to do this in StoryBoard editor.

Has anyone ever been able to achieve laying out views on top of each other for Watch App?

PS. I am aware of WKInterfaceGroup setBackgroundImage method. Since I want to do some animation inside WKInterfaceImage, setBackgroundImage is not going to woe for me

Dmitry Miller
  • 224
  • 2
  • 5
  • 1
    WKinterfaceGroup *can* do animations of the background image. Use `setBackgroundImageNamed:` and `startAnimatingWithImagesInRange:duration:repeatCount:` – Geoff Hackworth Mar 18 '15 at 17:20

2 Answers2

21

You can't layout WKInterfaceObjects on top of each other. You can render the labels over your image and then set it. You will have to render the labels for each frame of the animation. I do this in my watch app for buttons. I generate an image that is a large piece of my UI, I then generate each frame of the animations and overlay the button text for each frame. Then a cut up the image for each frame so that I can the animations on each of the buttons. When you use the app it looks like the image is animating under the buttons, when in actuality it is 4 different animations in 4 different buttons.

Edit

I figured I would add some more detail. Here is a screen capture of my app. I am guessing that you want to do something similar.

enter image description here

enter image description here

First in my storyboard you need to make sure that the spacing for your groups in zero.

enter image description here

To create this UI I use this helper class. I have edited this class to focus on just the needed parts.

typedef struct
{
    CGRect top;
    CGRect left;
    CGRect right;
    CGRect bottom;
} ButtonRects;

@interface GPWatchMapImage ()

@property (readwrite, nonatomic) UIImage* topImage;
@property (readwrite, nonatomic) UIImage* bottomImage;
@property (readwrite, nonatomic) UIImage* leftImage;
@property (readwrite, nonatomic) UIImage* rightImage;

@property (readwrite, nonatomic) CGRect topRect;
@property (readwrite, nonatomic) CGRect bottomRect;
@property (readwrite, nonatomic) CGRect leftRect;
@property (readwrite, nonatomic) CGRect rightRect;


@property (readwrite, nonatomic) UIImage* fullImageWithoutArrows;

@property BOOL addedArrows;


@end

@implementation GPWatchMapImage

-(instancetype)init
{
    self = [super init];
    if (self)
    {

    }
    return self;
}



-(UIImage*)leftImage
{
    if (_leftImage == nil)
    {
        [self breakUpForButtons];
    }
    return _leftImage;
}

-(UIImage*)rightImage
{
    if (_rightImage == nil)
    {
        [self breakUpForButtons];
    }
    return _rightImage;
}

-(UIImage*)topImage
{
    if (_topImage == nil)
    {
        [self breakUpForButtons];
    }
    return _topImage;
}

-(UIImage*)bottomImage
{
    if (_bottomImage == nil)
    {
        [self breakUpForButtons];
    }
    return _bottomImage;
}

-(UIImage*)fullImageWithoutArrows
{
    [self fullImage]; //make sure we have the full image
    if (_fullImageWithoutArrows != nil)
    {
        return _fullImageWithoutArrows;
    }
    return _fullImage;
}

-(UIImage*)fullImage
{
    if (_fullImage == nil)
    {
        //This is the rect to create the image in
        CGRect rect = CGRectMake(0, 0, self.watchSize.width, self.watchSize.height);

        //This is how I generate map images.  You will need to do something else
        self.generatedMapInfo = [[GPCustomMapMaker instance] makeCustomMapFromConfig:self.mapConfig];
        _fullImage = self.generatedMapInfo.mapImage;

    }

    if (self.showMapArrows && !self.addedArrows)
    {
        //Add the arrows
        [self addButtonArrowsToFullImage];
    }


    return _fullImage;
}


-(void)addButtonArrowsToFullImage
{
    self.addedArrows = YES;

    ButtonRects rects = [self buttonRects];
    UIImage* img = self.fullImage;
    self.fullImageWithoutArrows = img;  //save for animations
    UIGraphicsBeginImageContext(img.size);

    UIColor* color = [UIColor colorWithRed:.4 green:.4 blue:.4 alpha:.6];

    //CGSize arrowSize = CGSizeMake(24, 4);
    CGSize arrowSize = CGSizeMake(48, 8);
    CGFloat edgeOffset = 26;
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(ctx, 6);
    CGContextSetLineJoin(ctx, kCGLineJoinRound);
    CGContextSetLineCap(ctx, kCGLineCapRound);
    CGContextSetStrokeColorWithColor(ctx, color.CGColor);

    [img drawAtPoint:CGPointMake(0, 0)];


    //Left arrow
    CGPoint leftCenter = CGPointMake(rects.left.origin.x + edgeOffset, rects.left.origin.y + rects.left.size.height/2);
    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, leftCenter.x + arrowSize.height, leftCenter.y - arrowSize.width/2);
    CGContextAddLineToPoint(ctx, leftCenter.x, leftCenter.y);
    CGContextAddLineToPoint(ctx, leftCenter.x + arrowSize.height, leftCenter.y + arrowSize.width/2);
    CGContextStrokePath(ctx);

    CGPoint rightCenter = CGPointMake(rects.right.origin.x + rects.right.size.width - edgeOffset, rects.right.origin.y + rects.right.size.height/2);
    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, rightCenter.x, rightCenter.y - arrowSize.width/2);
    CGContextAddLineToPoint(ctx, rightCenter.x + arrowSize.height, rightCenter.y);
    CGContextAddLineToPoint(ctx, rightCenter.x, rightCenter.y + arrowSize.width/2);
    CGContextStrokePath(ctx);


    CGPoint topCenter = CGPointMake(rects.top.origin.x + rects.top.size.width/2, rects.top.origin.y + edgeOffset);
    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, topCenter.x - arrowSize.width/2, topCenter.y + arrowSize.height);
    CGContextAddLineToPoint(ctx, topCenter.x, topCenter.y);
    CGContextAddLineToPoint(ctx, topCenter.x + arrowSize.width/2, topCenter.y + arrowSize.height);
    CGContextStrokePath(ctx);

    CGPoint bottomCenter = CGPointMake(rects.bottom.origin.x + rects.bottom.size.width/2, rects.bottom.origin.y + rects.bottom.size.height - edgeOffset);
    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, bottomCenter.x - arrowSize.width/2, bottomCenter.y);
    CGContextAddLineToPoint(ctx, bottomCenter.x, bottomCenter.y + arrowSize.height);
    CGContextAddLineToPoint(ctx, bottomCenter.x + arrowSize.width/2, bottomCenter.y);
    CGContextStrokePath(ctx);


    UIImage* imgWithButtons = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    _fullImage = imgWithButtons;
}

-(UIImage*)subImageInRect:(CGRect)rect fromImage:(UIImage*)image
{
    UIGraphicsBeginImageContext(rect.size);

    [image drawInRect:CGRectMake(-rect.origin.x, -rect.origin.y, image.size.width, image.size.height)];

    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}

-(ButtonRects)buttonRects
{
    UIImage* img = self.fullImage;
    CGSize size = self.watchSize;
    CGFloat topHeight = size.height * .25;
    CGFloat bottomHeight = size.height * .25;
    CGFloat middleHeight = size.height * .5;

    CGRect topRect = CGRectMake(0, 0, size.width, topHeight);
    CGRect leftRect = CGRectMake(0, topHeight, img.size.width/2.0, middleHeight);
    CGRect rightRect = CGRectMake(img.size.width/2.0, topHeight, img.size.width/2.0, middleHeight);
    CGRect bottomRect = CGRectMake(0, topHeight + middleHeight, size.width, bottomHeight);

    ButtonRects rects;
    rects.top = topRect;
    rects.bottom = bottomRect;
    rects.left = leftRect;
    rects.right = rightRect;

    return rects;
}

-(void)breakUpForButtons
{
    UIImage* img = self.fullImage;
    ButtonRects rects = [self buttonRects];

    _topImage = [self subImageInRect:rects.top fromImage:img];
    _leftImage = [self subImageInRect:rects.left fromImage:img];
    _rightImage = [self subImageInRect:rects.right fromImage:img];
    _bottomImage = [self subImageInRect:rects.bottom fromImage:img];
}

@end

Next in my WKInterfaceController class I do this to make the animations.

typedef NS_ENUM(NSInteger, GPWatchImageAnimation)
{
    GPWatchImageAnimationNone,
    GPWatchImageAnimationSlideLeftToRight,
    GPWatchImageAnimationSlideRighToLeft,
    GPWatchImageAnimationSlideTopToBottom,
    GPWatchImageAnimationSlideBottomToTop,
    GPWatchImageAnimationZoomIn,
    GPWatchImageAnimationZoomOut
};

#define kNumImagesInMapAnimations 6
#define kMapAnimatinonDuration 0.4

...

-(void)updateMapImageWithAnimation:(GPWatchImageAnimation)animation
{
    GPWatchMapImage* previousImage = self.currentMapImage;

    //This gets the size of the full image that you want    
    CGSize size = [self mapSize];
    self.currentMapImage = [[GPWatchMapImage alloc] init];
    self.currentMapImage.watchSize = size;

        //Check if we have a previous image to animate from
        if (previousImage != nil && animation != GPWatchImageAnimationNone)
        {
            NSDictionary* animatedImage = [self animateFromImage:previousImage toImage:self.currentMapImage withAnimation:animation];
            [self.mapTopImage setImage:[animatedImage objectForKey:@"top"]];
            [self.mapLeftImage setImage:[animatedImage objectForKey:@"left"]];
            [self.mapRightImage setImage:[animatedImage objectForKey:@"right"]];
            [self.mapBottomImage setImage:[animatedImage objectForKey:@"bottom"]];

            [self.mapTopImage startAnimatingWithImagesInRange:NSMakeRange(0, kNumImagesInMapAnimations) duration:kMapAnimatinonDuration repeatCount:1];
            [self.mapLeftImage startAnimatingWithImagesInRange:NSMakeRange(0, kNumImagesInMapAnimations) duration:kMapAnimatinonDuration repeatCount:1];
            [self.mapRightImage startAnimatingWithImagesInRange:NSMakeRange(0, kNumImagesInMapAnimations) duration:kMapAnimatinonDuration repeatCount:1];
            [self.mapBottomImage startAnimatingWithImagesInRange:NSMakeRange(0, kNumImagesInMapAnimations) duration:kMapAnimatinonDuration repeatCount:1];
        }
        else
        {
            [self.mapTopImage setImage:self.currentMapImage.topImage];
            [self.mapLeftImage setImage:self.currentMapImage.leftImage];
            [self.mapRightImage setImage:self.currentMapImage.rightImage];
            [self.mapBottomImage setImage:self.currentMapImage.bottomImage];
        }

}

-(NSDictionary*)animateFromImage:(GPWatchMapImage*)fromImage toImage:(GPWatchMapImage*)toImage withAnimation:(GPWatchImageAnimation)animation
{
    NSMutableArray* topAnimatedImages = [[NSMutableArray alloc] initWithCapacity:kNumImagesInMapAnimations];
    NSMutableArray* bottomAnimatedImages = [[NSMutableArray alloc] initWithCapacity:kNumImagesInMapAnimations];
    NSMutableArray* leftAnimatedImages = [[NSMutableArray alloc] initWithCapacity:kNumImagesInMapAnimations];
    NSMutableArray* rightAnimatedImages = [[NSMutableArray alloc] initWithCapacity:kNumImagesInMapAnimations];

    CGSize size = fromImage.fullImage.size;
    for (int step = 0; step < kNumImagesInMapAnimations; step++)
    {
        UIGraphicsBeginImageContext(size);

        //render this step
        if (animation == GPWatchImageAnimationSlideLeftToRight)
        {
            CGFloat stepSize = (size.width/2)/kNumImagesInMapAnimations;
            CGPoint fromPoint = CGPointMake(-(step+1)*stepSize, 0);
            CGPoint toPoint = CGPointMake(size.width/2 - (step+1)*stepSize, 0);
            [fromImage.fullImageWithoutArrows drawAtPoint:fromPoint];
            [toImage.fullImageWithoutArrows drawAtPoint:toPoint];
        }
        else if (animation == GPWatchImageAnimationSlideRighToLeft)
        {
            CGFloat stepSize = (size.width/2)/kNumImagesInMapAnimations;
            CGPoint fromPoint = CGPointMake((step+1)*stepSize, 0);
            CGPoint toPoint = CGPointMake(-size.width/2 + (step+1)*stepSize, 0);
            [fromImage.fullImageWithoutArrows drawAtPoint:fromPoint];
            [toImage.fullImageWithoutArrows drawAtPoint:toPoint];
        }
        else if (animation == GPWatchImageAnimationSlideTopToBottom)
        {
            CGFloat stepSize = (size.height/2)/kNumImagesInMapAnimations;
            CGPoint fromPoint = CGPointMake(0, (step+1)*stepSize);
            CGPoint toPoint = CGPointMake(0, -size.height/2 + (step+1)*stepSize);
            [fromImage.fullImageWithoutArrows drawAtPoint:fromPoint];
            [toImage.fullImageWithoutArrows drawAtPoint:toPoint];
        }
        else if (animation == GPWatchImageAnimationSlideBottomToTop)
        {
            CGFloat stepSize = (size.height/2)/kNumImagesInMapAnimations;
            CGPoint fromPoint = CGPointMake(0, -(step+1)*stepSize);
            CGPoint toPoint = CGPointMake(0, size.height/2 - (step+1)*stepSize);
            [fromImage.fullImageWithoutArrows drawAtPoint:fromPoint];
            [toImage.fullImageWithoutArrows drawAtPoint:toPoint];
        }
        else if (animation == GPWatchImageAnimationZoomOut)
        {
            CGFloat yStepSize = (size.height/2)/kNumImagesInMapAnimations;
            CGFloat xStepSize = (size.width/2)/kNumImagesInMapAnimations;
            //CGRect fromRect = CGRectMake((step + 1)*xStepSize, (step + 1)*yStepSize, size.width - 2*(step + 1)*xStepSize, size.height - 2*(step + 1)*yStepSize);
            CGRect toRect = CGRectMake(-size.width/2 + (step+1)*xStepSize, -size.height/2 + (step+1)*yStepSize, size.width + 2*(kNumImagesInMapAnimations - step - 1)*xStepSize, size.height + 2*(kNumImagesInMapAnimations - step - 1)*yStepSize);
            [toImage.fullImageWithoutArrows drawInRect:toRect];

            //double alpha = (double)(kNumImagesInMapAnimations - step - 1)/(double)kNumImagesInMapAnimations;
            //CGContextSetAlpha(UIGraphicsGetCurrentContext(), alpha);
            //[fromImage.fullImageWithoutArrows drawInRect:fromRect];
            //CGContextSetAlpha(UIGraphicsGetCurrentContext(), 1.0);
        }
        else if (animation == GPWatchImageAnimationZoomIn)
        {
            if (step == kNumImagesInMapAnimations -1)
            {
                [toImage.fullImageWithoutArrows drawAtPoint:CGPointMake(0, 0)];
            }
            else
            {
                CGFloat yStepSize = (size.height/2)/kNumImagesInMapAnimations;
                CGFloat xStepSize = (size.width/2)/kNumImagesInMapAnimations;
                CGRect fromRect = CGRectMake(-(step + 1)*xStepSize, -(step + 1)*yStepSize, size.width + 2*(step + 1)*xStepSize, size.height + 2*(step + 1)*yStepSize);
                //CGRect toRect = CGRectMake(-size.width/2 + (step+1)*xStepSize, -size.height/2 + (step+1)*yStepSize, size.width + 2*(kNumImagesInMapAnimations - step - 1)*xStepSize, size.height + 2*(kNumImagesInMapAnimations - step - 1)*yStepSize);
                [fromImage.fullImageWithoutArrows drawInRect:fromRect];
                //[toImage.fullImageWithoutArrows drawInRect:fromRect];
            }
        }
        else
        {
            [toImage.fullImageWithoutArrows drawAtPoint:CGPointMake(0, 0)];
        }

        UIImage* stepImg = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();


        //Get each of the pieces of the image
        GPWatchMapImage* mapImage = [[GPWatchMapImage alloc] init];
        mapImage.showMapArrows = self.showMapArrows;
        mapImage.fullImage = stepImg;
        mapImage.watchSize = [self mapSize];
        [topAnimatedImages addObject:mapImage.topImage];
        [bottomAnimatedImages addObject:mapImage.bottomImage];
        [leftAnimatedImages addObject:mapImage.leftImage];
        [rightAnimatedImages addObject:mapImage.rightImage];
    }

    UIImage* topAnimatedImage = [UIImage animatedImageWithImages:topAnimatedImages duration:kMapAnimatinonDuration];
    UIImage* bottomAnimatedImage = [UIImage animatedImageWithImages:bottomAnimatedImages duration:kMapAnimatinonDuration];
    UIImage* leftAnimatedImage = [UIImage animatedImageWithImages:leftAnimatedImages duration:kMapAnimatinonDuration];
    UIImage* rightAnimatedImage = [UIImage animatedImageWithImages:rightAnimatedImages duration:kMapAnimatinonDuration];
    return @{@"top": topAnimatedImage, @"bottom": bottomAnimatedImage, @"left": leftAnimatedImage, @"right": rightAnimatedImage};
}
Stephen Johnson
  • 5,293
  • 1
  • 23
  • 37
  • 2
    Stephen. This is a really interesting approach. What I am working on is a circular gage with some labels in the middle. The part that needs to be animated is a circular bar of the gage. While considering your approach of rendering everything as an image, I was thinking of the performance implications. Although generating all the frames doesn't take much time, I would imagine that transfering them from a phone to the watch device over bluetooth can take some time. Do you have any insight on performance implications of your approach? – Dmitry Miller Dec 19 '14 at 20:16
  • 1
    That is also my worry. I don't have any way to know what the performance will be. I can run the code that generates the animation frames on a device, but I don't know what the transfer will be like. – Stephen Johnson Dec 19 '14 at 21:49
  • 2
    In Xcode 6.2 beta 4 sending the animation from the extension to the watch is very slow. It looks like it is too much information to send. – Stephen Johnson Jan 28 '15 at 13:38
  • @StephenJohnson, This is very clever. Respect! But why do you need to split the animation into four pieces? Why can't you just have a single (full size) animation of the background image on the top-level group (the one that is highlighted in your Xcode storyboard screenshot). The other buttons could then be groups that contain images which just have the arrows on a transparent background to show the map underneath. Or am I missing something??? – Geoff Hackworth Mar 18 '15 at 17:29
  • You are not missing anything. I realized I overworked this solution a few weeks ago and changed it to do just what you said. I am still finding that it is too slow to send the animation to the watch with the latest Xcode 6.2. – Stephen Johnson Mar 18 '15 at 17:38
  • 1
    Thanks. You're really pushing the envelope here and I can imagine there's a lot of data to throw at the watch. Got to admire your ingenuity. By the way - you might want to send the details of your app to http://watchaware.com. I'm sure they'd love it. – Geoff Hackworth Mar 18 '15 at 17:58
  • @StephenJohnson How do you display that activity indicator in the status bar? Never mind, that was the network activity indicator I was unaware of. I recently started on WatchKit. – Matt May 20 '15 at 06:30
5

In InterfaceBuilder: You can just use a Group and set a Backgroundimage from your assets. When you then add an WKInterfaceImage into that group you have two layers to work with (if two layers are enough for you).

Corona
  • 685
  • 1
  • 10
  • 15