Objective c 如何在NSAttributed字符串的u外u划?

Objective c 如何在NSAttributed字符串的u外u划?,objective-c,cocoa,nsattributedstring,Objective C,Cocoa,Nsattributedstring,我一直在nsattributestring对象上使用NSStrokeWidthAttributeName在绘制文本时在文本周围放置一个轮廓。问题是笔划位于文本的填充区域内。当文本较小(例如1像素厚)时,笔划会使文本难以阅读。我真正想要的是在外面打一针。有办法吗 我尝试了一个没有偏移和模糊的NSShadow,但是它太模糊,很难看到。如果有一种方法可以在不模糊的情况下增加阴影的大小,那也行。虽然可能还有其他方法,但实现这一点的一种方法是首先只使用笔划绘制字符串,然后仅使用填充绘制字符串,直接覆盖先前

我一直在
nsattributestring
对象上使用
NSStrokeWidthAttributeName
在绘制文本时在文本周围放置一个轮廓。问题是笔划位于文本的填充区域内。当文本较小(例如1像素厚)时,笔划会使文本难以阅读。我真正想要的是在外面打一针。有办法吗


我尝试了一个没有偏移和模糊的
NSShadow
,但是它太模糊,很难看到。如果有一种方法可以在不模糊的情况下增加阴影的大小,那也行。

虽然可能还有其他方法,但实现这一点的一种方法是首先只使用笔划绘制字符串,然后仅使用填充绘制字符串,直接覆盖先前绘制的内容。(AdobeInDesign实际上内置了这个功能,它似乎只将笔划应用于字母的外部,这有助于提高可读性)

这只是一个示例视图,展示了如何实现这一点(受以下启发):

首先设置属性:

@implementation MDInDesignTextView

static NSMutableDictionary *regularAttributes = nil;
static NSMutableDictionary *indesignBackgroundAttributes = nil;
static NSMutableDictionary *indesignForegroundAttributes = nil;

- (void)drawRect:(NSRect)frame {
    NSString *string = @"Got stroke?";
    if (regularAttributes == nil) {
        regularAttributes = [[NSMutableDictionary
    dictionaryWithObjectsAndKeys:
        [NSFont systemFontOfSize:64.0],NSFontAttributeName,
        [NSColor whiteColor],NSForegroundColorAttributeName,
        [NSNumber numberWithFloat:-5.0],NSStrokeWidthAttributeName,
        [NSColor blackColor],NSStrokeColorAttributeName, nil] retain];
    }

    if (indesignBackgroundAttributes == nil) {
        indesignBackgroundAttributes = [[NSMutableDictionary
        dictionaryWithObjectsAndKeys:
        [NSFont systemFontOfSize:64.0],NSFontAttributeName,
        [NSNumber numberWithFloat:-5.0],NSStrokeWidthAttributeName,
        [NSColor blackColor],NSStrokeColorAttributeName, nil] retain];
    }

    if (indesignForegroundAttributes == nil) {
        indesignForegroundAttributes = [[NSMutableDictionary
        dictionaryWithObjectsAndKeys:
        [NSFont systemFontOfSize:64.0],NSFontAttributeName,
        [NSColor whiteColor],NSForegroundColorAttributeName, nil] retain];
    }

    [[NSColor grayColor] set];
    [NSBezierPath fillRect:frame];

    // draw top string
    [string drawAtPoint:
        NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 200.0)
        withAttributes:regularAttributes];

    // draw bottom string in two passes
    [string drawAtPoint:
        NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 140.0)
        withAttributes:indesignBackgroundAttributes];
    [string drawAtPoint:
        NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 140.0)
        withAttributes:indesignForegroundAttributes];
}

@end
这将产生以下输出:

现在,它并不完美,因为glyph有时会落在分数边界上,但是,它看起来肯定比默认的要好


如果性能是一个问题,您总是可以考虑降低到一个稍低的级别,例如CoreGraphics或CoreText。

只需将我基于@NSGod答案的解决方案留在这里,结果与在
UILabel中进行适当的定位是一样的。

在iOS 14上使用默认系统字体笔划字母时出现错误时,它也很有用(另请参阅)

错误:


非常详细的回答。谢谢你花时间!简单。明亮的而且应该是完全没有必要的。。。(应该已经存在对此的支持)
@interface StrokedTextLabel : UILabel
@end

/**
 * https://stackoverflow.com/a/4468880/3004003
 */
@implementation StrokedTextLabel

- (void)drawTextInRect:(CGRect)rect
{
    if (!self.attributedText) {
        [super drawTextInRect:rect];
        return;
    }

    NSMutableAttributedString *attributedText = self.attributedText.mutableCopy;
    [attributedText enumerateAttributesInRange:NSMakeRange(0, attributedText.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange range, BOOL *stop) {
        if (attrs[NSStrokeWidthAttributeName]) {
            // 1. draw underlying stroked string
            // use doubled stroke width to simulate outer border, because border is being stroked
            // in both outer & inner directions with half width
            CGFloat strokeWidth = [attrs[NSStrokeWidthAttributeName] floatValue] * 2;
            [attributedText addAttributes:@{NSStrokeWidthAttributeName : @(strokeWidth)} range:range];
            self.attributedText = attributedText;
            // perform default drawing
            [super drawTextInRect:rect];

            // 2. draw unstroked string above
            NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];
            style.alignment = self.textAlignment;

            [attributedText addAttributes:@{
                NSStrokeWidthAttributeName : @(0),
                NSForegroundColorAttributeName : self.textColor,
                NSFontAttributeName : self.font,
                NSParagraphStyleAttributeName : style
            } range:range];

            // we use here custom bounding rect detection method instead of
            // [attributedText boundingRectWithSize:...] because the latter gives incorrect result
            // in this case
            CGRect textRect = [self boundingRectWithAttributedString:attributedText forCharacterRange:NSMakeRange(0, attributedText.length)];
            [attributedText boundingRectWithSize:rect.size options:NSStringDrawingUsesLineFragmentOrigin
                                         context:nil];
            // adjust vertical position because returned bounding rect has zero origin
            textRect.origin.y = (rect.size.height - textRect.size.height) / 2;
            [attributedText drawInRect:textRect];
        }
    }];
}

/**
 * https://stackoverflow.com/a/20633388/3004003
 */
- (CGRect)boundingRectWithAttributedString:(NSAttributedString *)attributedString
                         forCharacterRange:(NSRange)range
{
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    [textStorage addLayoutManager:layoutManager];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
    textContainer.lineFragmentPadding = 0;
    [layoutManager addTextContainer:textContainer];

    NSRange glyphRange;

    // Convert the range for glyphs.
    [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];

    return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
}

@end
import Foundation
import UIKit

/// https://stackoverflow.com/a/4468880/3004003
@objc(MUIStrokedTextLabel)
public class StrokedTextLabel : UILabel {

    override public func drawText(in rect: CGRect) {

        guard let attributedText = attributedText?.mutableCopy() as? NSMutableAttributedString else {
            super.drawText(in: rect)
            return
        }

        attributedText.enumerateAttributes(in: NSRange(location: 0, length: attributedText.length), options: [], using: { attrs, range, stop in
            guard let strokeWidth = attrs[NSAttributedString.Key.strokeWidth] as? CGFloat else {
                return
            }

            // 1. draw underlying stroked string
            // use doubled stroke width to simulate outer border, because border is being stroked
            // in both outer & inner directions with half width
            attributedText.addAttributes([
                NSAttributedString.Key.strokeWidth: strokeWidth * 2
            ], range: range)
            self.attributedText = attributedText
            // perform default drawing
            super.drawText(in: rect)

            // 2. draw unstroked string above
            let style = NSMutableParagraphStyle()
            style.alignment = textAlignment

            let attributes = [
                NSAttributedString.Key.strokeWidth: NSNumber(value: 0),
                NSAttributedString.Key.foregroundColor: textColor ?? UIColor.black,
                NSAttributedString.Key.font: font ?? UIFont.systemFont(ofSize: 17),
                NSAttributedString.Key.paragraphStyle: style
            ]

            attributedText.addAttributes(attributes, range: range)

            // we use here custom bounding rect detection method instead of
            // [attributedText boundingRectWithSize:...] because the latter gives incorrect result
            // in this case
            var textRect = boundingRect(with: attributedText, forCharacterRange: NSRange(location: 0, length: attributedText.length))
            attributedText.boundingRect(
                    with: rect.size,
                    options: .usesLineFragmentOrigin,
                    context: nil)
            // adjust vertical position because returned bounding rect has zero origin
            textRect.origin.y = (rect.size.height - textRect.size.height) / 2
            attributedText.draw(in: textRect)
        })
    }

    /// https://stackoverflow.com/a/20633388/3004003
    private func boundingRect(
            with attributedString: NSAttributedString?,
            forCharacterRange range: NSRange
    ) -> CGRect {
        guard let attributedString = attributedString else {
            return .zero
        }
        let textStorage = NSTextStorage(attributedString: attributedString)
        let layoutManager = NSLayoutManager()
        textStorage.addLayoutManager(layoutManager)
        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding = 0
        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        // Convert the range for glyphs.
        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    }

}