问题
I'm trying to create a UI element on iOS that expands or contracts to fit dynamic, often multiline text. I need to put an image underneath the text that looks a lot like a message bubble. However, I only need one of these in my entire UI, so a table view seems overkill - unless reasonably necessary.
I've tried to use a UILabel for this purpose, but it seems to have very limited support for background images. I can only get it to tile the image, which is not what I want.
It seems that a UIButton is the simplest construct that should reasonably do what I'm asking, but I cannot get it to work. Here's a distilled form of the code in an otherwise empty single view app.
- (void)viewDidLoad {
[super viewDidLoad];
NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
UIImage *image = [[UIImage imageNamed:@"speak_bubble_solid"] resizableImageWithCapInsets:UIEdgeInsetsMake(16, 24, 30, 12)];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setBackgroundImage:image forState:UIControlStateNormal];
[button setTitle:text forState:UIControlStateNormal];
button.titleLabel.textColor = [UIColor whiteColor];
button.titleLabel.textAlignment = NSTextAlignmentLeft;
button.titleLabel.numberOfLines = 0;
button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
button.contentEdgeInsets = UIEdgeInsetsMake(16, 10, 30, 10);
button.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:button];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[button]-|"
options:NSLayoutFormatAlignAllTop
metrics:0
views:NSDictionaryOfVariableBindings(button)]];
}
This is how it looks when I run the app:

Obviously, what I'm looking for is the button to expand to contain its content. So even the initial render is wrong, but I also need it to be sized correctly when I change the text dynamically later.
How can I create this? I need to see the answer in code, since I'm still very new in iOS development.
回答1:
Actually what I would use is a label and an image. Not a background image: an image, in an image view.
Make the label wrap, and grow or shrink vertically depending on the size of its contents. That's easy and is a ready, previously solved problem.
Make the label's background clear.
Constrain the image view to the same position and size as the label, behind it.
Make sure the image view's content mode is scale-to-fill, so that the image will be always be sized exactly to the size of the image view.
Give the image view an image that is "stretchable" so that it will look right no matter what size the image view is.
Here are some screen shots of what I got when I tried this, with different lengths of self-wrapping label text:


That seems pretty close to the sort of thing you are after. The tricky part, in my implementation, is adjusting the size / position of the image view in response to changes in the size of the label as it wraps and changes its height in response to its content. The way I worked it out was to set constraints from the image view to the label, making the image view bigger than the label so that the balloon appears around the outside of the label; then, I added this code to my view controller to adjust the image view (iv
) further in real time in relation to the layout of the label (lab
):
override func viewDidLayoutSubviews() {
iv.bounds.size.height = lab.bounds.size.height + 60
iv.frame.origin.y = lab.frame.origin.y - 20
}
EDIT: But in fact no code is needed; it can be done entirely with constraints, as I demonstrate in this github example: https://github.com/mattneub/MessageBubble
回答2:
Another possible approach is to use a drawn shape rather than an image as the background. This requires more code than Matt's approach, but might give you more flexibility if you want to change the color or the shape of the point. I created a UIView class called RDLabelView that draws the background shape, and adds a label as a subview. It has a text property so you can set its text just like you would for a label.
#import "RDLabelView.h"
@interface RDLabelView ()
@property (strong,nonatomic) UILabel *label;
@property (strong,nonatomic) CAShapeLayer *bubble;
@end
@implementation RDLabelView
-(void)awakeFromNib {
self.tapper = [UITapGestureRecognizer new];
[self addGestureRecognizer:self.tapper];
self.bubble = [CAShapeLayer layer];
self.bubble.fillColor = [UIColor colorWithRed:.5 green:1 blue:1 alpha:1].CGColor;
[self.layer addSublayer:self.bubble];
self.label = [UILabel new];
self.label.numberOfLines = 0;
self.label.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.label];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-10-[label]-10-|" options:0 metrics:nil views:@{@"label":self.label}]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-10-[label]-10-|" options:0 metrics:nil views:@{@"label":self.label}]];
}
-(void)setColor:(UIColor *)color { // The color property is declared in the .h file to allow color changes to the bubble
_color = color;
self.bubble.fillColor = _color.CGColor;
}
-(void)setText:(NSString *)text { // The text property is declared in the .h file to allow you to set the text as you would for a UILabel
_text = text;
self.label.text = text;
}
-(UIBezierPath *)appendShapeToRoundedRect:(UIBezierPath *) shape {
CGPoint lowerLeftCorner = CGPointMake(0, shape.bounds.size.height);
CGPoint pointToStart = CGPointMake(lowerLeftCorner.x + 30, lowerLeftCorner.y);
UIBezierPath * arrow = [UIBezierPath bezierPath];
[arrow moveToPoint:pointToStart];
[arrow addLineToPoint:CGPointMake(pointToStart.x, pointToStart.y + 20)];
[arrow addLineToPoint:CGPointMake(pointToStart.x + 10, pointToStart.y)];
[shape appendPath:arrow];
return shape;
}
-(void)layoutSubviews {
[super layoutSubviews];
UIBezierPath *shape = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:16];
shape = [self appendShapeToRoundedRect:shape];
self.bubble.path = shape.CGPath;
}
来源:https://stackoverflow.com/questions/29481236/how-can-i-create-a-uibutton-supporting-dynamic-multiline-text-with-image-backgro