问题
How can I achieve this kind of style in iOS?

I tried using NSAttributedString
and setting NSBackgroundColorAttributeName
as blackColor
but then it doesn't have a clearColor
space between the lines. I also tried to set a lineSpacing
to the NSMutableParagraphStyle
but still, the style looks like one big black block.
I just need a hint in which way to go... thanks!
In other words, not something like this:

回答1:
After some research I found the best solution for what I needed. The solution below is only iOS7+.
First we add this to - (void)drawRect:(CGRect)rect
of your UITextView
subclass.
- (void)drawRect:(CGRect)rect
/// Position each line behind each line.
[self.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, self.text.length) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
/// The frame of the rectangle.
UIBezierPath *rectanglePath = [UIBezierPath bezierPathWithRect:CGRectMake(usedRect.origin.x, usedRect.origin.y+3, usedRect.size.width, usedRect.size.height-4)];
/// Set the background color for each line.
[UIColor.blackColor setFill];
/// Build the rectangle.
[rectanglePath fill];
}];
}];
Then we set the line spacing for the UITextView:
- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect
{
return 15;
}
The method above is only called if you set the NSLayoutManagerDelegate
. You could do that in your init
, initWithFrame
and initWithCode
methods like this:
self.layoutManager.delegate = self;
Also don't forget to declare that your subclass is a delegate in your .h
file:
@interface YOUR_SUBCLASS_OF_UITEXTVIEW : UITextView <NSLayoutManagerDelegate>
回答2:
Swift 3 version of @Fabio's solution :
class splitedTextView: UITextView, NSLayoutManagerDelegate {
override func awakeFromNib() {
self.layoutManager.delegate = self
}
override func draw(_ rect: CGRect) {
self.layoutManager.enumerateLineFragments(forGlyphRange: NSMakeRange(0, self.text.characters.count)) { (rect, usedRect, textContainer, glyphRange, Bool) in
let rectanglePath = UIBezierPath(rect: CGRect(x: usedRect.origin.x, y: usedRect.origin.y+3, width: usedRect.size.width, height: usedRect.size.height-4))
UIColor.black.setFill()
rectanglePath.fill()
}
}
func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat {
return 15
}
}
You can also create a variable inside your textView
subclass to manage rectangle's color :
var color: UIColor?
And then use it instead of default black in your textView
subclass :
self.color?.setFill()
Also if you do that, don't forget to use setNeedsDisplay()
to redraw your textView
and apply your custom color.
回答3:
EDIT :
For your requirment you need to use different UILabel
for each line
and manage spacing
by setting frame
.
This is working for me to have spacing
between lines
in multiline UILabel
.
NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] initWithString:yourlblText.text];
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
[style setLineSpacing:16];
[attrString addAttribute:NSParagraphStyleAttributeName
value:style
range:NSMakeRange(0, yourlblText.text.length)];
yourlblText.attributedText = attrString;
Also check iOS 6 multiline label line spacing
回答4:
Simply add a new blank line with a paragraph style with the desired line height. In my Swift example, I just append an interlineAttributedString to the original attributedString:
let interlineStyle = NSMutableParagraphStyle()
interlineStyle.maximumLineHeight = 5.0
let interlineAttributedString = NSMutableAttributedString(string: "\n", attributes: [NSParagraphStyleAttributeName: interlineStyle])
attributedString.appendAttributedString(interlineAttributedString)
The extra \n can also be part of the original attributedString. In that case, you would use addAttribute
with the appropriate range.
I tested on iOS 9 only, but I'm pretty sure it works well on previous iOS versions. It works perfectly in a UITextView. It is much easier to implement than using many UILabels or drawing the background rectangles.
回答5:
My approach was to create a UIView and add the label on it, constrain to Vertical top and the calculate the number of lines using the font size and line height and then create a CALayer for each line and add those to the UIView.
You can check my implementation here
func setBorderLabel() {
let str = "This is label style with solid highlight and have gap between the texts, uses a combination of UIView and UILabel, you can have borders also per line... check comments in the code"
let trimmedString = str.trimmingCharacters(in: .whitespacesAndNewlines)
let string = NSMutableAttributedString(string: trimmedString)
borderLabel.attributedText = string
let labelWidth: CGFloat = borderLabel.frame.size.width
var lineCount: Float = 0.0
let textSize = CGSize(width: labelWidth, height: CGFloat(MAXFLOAT))
let rHeight: Float = Float(borderLabel.sizeThatFits(textSize).height)
//var rWidth: Int = lroundf(Float(borderLabel.sizeThatFits(textSize).width))
let charSize: Float = Float(borderLabel.font.lineHeight)
lineCount = (rHeight / charSize).rounded()
var count = 0
// This needs a bit of trial and error, you need to play with the FONT_SIZE value to get the desired effect
for i in stride(from: 0, to: Int(borderLabel.frame.size.height) , by: FONT_SIZE) {
if count >= Int(lineCount){
break
}
let border = CALayer()
border.borderColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)
border.frame = CGRect(x: 0, y: CGFloat(i), width: borderLabel.frame.size.width, height: CGFloat(FONT_SIZE - 1) )
// Uncomment the below lines to have borders per line.
//border.borderWidth = 1.0
//border.cornerRadius = 6.0
border.backgroundColor = #colorLiteral(red: 0.9411764741, green: 0.4980392158, blue: 0.3529411852, alpha: 1)
borderLabel.layer.addSublayer(border)
bgView.layer.insertSublayer(border, at: 0)
count = count + 1
borderLabel.layer.masksToBounds = true
}
}
回答6:
For iOS 11 attributed string has a problem with this kind of UI behavior. I've created UITextView subclass with space and bgColor property. Hope this helps too. The answer is just an extension to @Fabio solution.
class SpacingTextView: UITextView, NSLayoutManagerDelegate {
private var textHolder: String?
var spacing: CGFloat = 3
var bgColor: UIColor = .white
override var attributedText: NSAttributedString! {
didSet {
self.textHolder = self.attributedText.string
self.setNeedsDisplay()
}
}
override var text: String! {
didSet {
self.textHolder = self.attributedText.string
self.setNeedsDisplay()
}
}
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
self.configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.configure()
}
private func configure() {
self.layoutManager.delegate = self
self.backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let txt = self.textHolder else {
return
}
let textRange = NSRange(location: 0, length: txt.count)
self.layoutManager.enumerateLineFragments(forGlyphRange: textRange) { (rect, usedRect, _, _, _) in
var bgRect = usedRect
bgRect.origin.y += self.spacing / 2
bgRect.size.height -= self.spacing
let bezierPath = UIBezierPath(rect: bgRect)
self.bgColor.setFill()
bezierPath.fill()
bezierPath.close()
}
}
func layoutManager(_ layoutManager: NSLayoutManager,
lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat {
return rect.size.height
}
func layoutManager(_ layoutManager: NSLayoutManager,
shouldUse action: NSLayoutManager.ControlCharacterAction,
forControlCharacterAt charIndex: Int) -> NSLayoutManager.ControlCharacterAction {
return .lineBreak
}
}
来源:https://stackoverflow.com/questions/27360006/uitextview-uilabel-with-background-but-with-spacing-between-lines