Apple の AppStore にあるような「続きを読む」機能を実装しています。ただし、複数行を使用していUILabel
ます。Apple の AppStore を見ると、最後に表示されている行の幅を「詳細」テキストに合わせて縮小し、末尾を切り捨てるにはどうすればよいでしょうか (画像を参照)。
6 に答える
少なくとも私が行った限られた量のテストでは、これはうまくいくようです。public メソッドは 2 つあります。複数のラベルがすべて同じ行数である場合は、短い方を使用できます。上部の kNumberOfLines を必要なものに合わせて変更するだけです。異なるラベルの行数を渡す必要がある場合は、より長い方法を使用してください。IB で作成するラベルのクラスを必ず RDLabel に変更してください。setText: の代わりにこれらのメソッドを使用します。これらのメソッドは、必要に応じてラベルの高さを kNumberOfLines に拡張し、それでも切り詰められている場合は、タッチ時に文字列全体に収まるように拡張します。現在、ラベルのどこでもタッチできます。それを変更するのはそれほど難しくないはずなので、 ...Mer の近くに触れるだけで拡張が発生します。
#import "RDLabel.h"
#define kNumberOfLines 2
#define ellipsis @"...Mer ▾ "
@implementation RDLabel {
NSString *string;
}
#pragma Public Methods
- (void)setTruncatingText:(NSString *) txt {
[self setTruncatingText:txt forNumberOfLines:kNumberOfLines];
}
- (void)setTruncatingText:(NSString *) txt forNumberOfLines:(int) lines{
string = txt;
self.numberOfLines = 0;
NSMutableString *truncatedString = [txt mutableCopy];
if ([self numberOfLinesNeeded:truncatedString] > lines) {
[truncatedString appendString:ellipsis];
NSRange range = NSMakeRange(truncatedString.length - (ellipsis.length + 1), 1);
while ([self numberOfLinesNeeded:truncatedString] > lines) {
[truncatedString deleteCharactersInRange:range];
range.location--;
}
[truncatedString deleteCharactersInRange:range]; //need to delete one more to make it fit
CGRect labelFrame = self.frame;
labelFrame.size.height = [@"A" sizeWithFont:self.font].height * lines;
self.frame = labelFrame;
self.text = truncatedString;
self.userInteractionEnabled = YES;
UITapGestureRecognizer *tapper = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(expand:)];
[self addGestureRecognizer:tapper];
}else{
CGRect labelFrame = self.frame;
labelFrame.size.height = [@"A" sizeWithFont:self.font].height * lines;
self.frame = labelFrame;
self.text = txt;
}
}
#pragma Private Methods
-(int)numberOfLinesNeeded:(NSString *) s {
float oneLineHeight = [@"A" sizeWithFont:self.font].height;
float totalHeight = [s sizeWithFont:self.font constrainedToSize:CGSizeMake(self.bounds.size.width, CGFLOAT_MAX) lineBreakMode:NSLineBreakByWordWrapping].height;
return nearbyint(totalHeight/oneLineHeight);
}
-(void)expand:(UITapGestureRecognizer *) tapper {
int linesNeeded = [self numberOfLinesNeeded:string];
CGRect labelFrame = self.frame;
labelFrame.size.height = [@"A" sizeWithFont:self.font].height * linesNeeded;
self.frame = labelFrame;
self.text = string;
}
この投稿は 2013 年のものなので、@rdelmar からの非常に優れたソリューションの Swift 実装を提供したいと思いました。
UILabel のサブクラスを使用していることを考慮してください。
private let kNumberOfLines = 2
private let ellipsis = " MORE"
private var originalString: String! // Store the original text in the init
private func getTruncatingText() -> String {
var truncatedString = originalString.mutableCopy() as! String
if numberOfLinesNeeded(truncatedString) > kNumberOfLines {
truncatedString += ellipsis
var range = Range<String.Index>(
start: truncatedString.endIndex.advancedBy(-(ellipsis.characters.count + 1)),
end: truncatedString.endIndex.advancedBy(-ellipsis.characters.count)
)
while numberOfLinesNeeded(truncatedString) > kNumberOfLines {
truncatedString.removeRange(range)
range.startIndex = range.startIndex.advancedBy(-1)
range.endIndex = range.endIndex.advancedBy(-1)
}
}
return truncatedString
}
private func getHeightForString(str: String) -> CGFloat {
return str.boundingRectWithSize(
CGSizeMake(self.bounds.size.width, CGFloat.max),
options: [.UsesLineFragmentOrigin, .UsesFontLeading],
attributes: [NSFontAttributeName: font],
context: nil).height
}
private func numberOfLinesNeeded(s: String) -> Int {
let oneLineHeight = "A".sizeWithAttributes([NSFontAttributeName: font]).height
let totalHeight = getHeightForString(s)
return Int(totalHeight / oneLineHeight)
}
func expend() {
var labelFrame = self.frame
labelFrame.size.height = getHeightForString(originalString)
self.frame = labelFrame
self.text = originalString
}
func collapse() {
let truncatedText = getTruncatingText()
var labelFrame = self.frame
labelFrame.size.height = getHeightForString(truncatedText)
self.frame = labelFrame
self.text = truncatedText
}
古いソリューションとは異なり、これはあらゆる種類のテキスト属性 (NSParagraphStyleAttributeName など) に対しても同様に機能します。
批評・コメントはお気軽にどうぞ。@rdelmarに再び感謝します。
これには複数の方法がありますが、テキストの表示方法を完全に制御できるので、CoreText を排他的に使用するのが最もエレガントです。
これは、CoreText を使用してラベルを再作成し、ラベルの終了位置を決定してから、ラベルのテキスト文字列を適切な場所で切り取るハイブリッド オプションです。
NSMutableAttributedString *atrStr = [[NSAttributedString alloc] initWithString:label.text];
NSNumber *kern = [NSNumber numberWithFloat:0];
NSRange full = NSMakeRange(0, [atrStr string].length);
[atrStr addAttribute:(id)kCTKernAttributeName value:kern range:full];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)atrStr);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, label.frame);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CFArrayRef lines = CTFrameGetLines(frame);
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, label.numberOfLines-1);
CFRange r = CTLineGetStringRange(line);
これにより、ラベル テキストの最後の行の範囲が得られます。そこから、それを切り取り、必要な場所に省略記号を配置するのは簡単です。
最初の部分は、UILabel の動作を複製するために必要なプロパティを含む属性付き文字列を作成します (100% ではないかもしれませんが、十分に近いはずです)。次に、フレームセッターとフレームを作成し、フレームのすべての行を取得します。そこから、ラベルの最後に予想される行の範囲を抽出します。
これは明らかにある種のハックであり、前述したように、テキストの外観を完全に制御したい場合は、そのラベルの純粋な CoreText 実装を使用することをお勧めします。
部分文字列の計算を高速化するために二分探索を使用して、Swift 4 で UILabel 拡張機能を作成しました。
もともとは @paul-slm によるソリューションに基づいていましたが、かなり分岐しています
extension UILabel {
func getTruncatingText(originalString: String, newEllipsis: String, maxLines: Int?) -> String {
let maxLines = maxLines ?? self.numberOfLines
guard maxLines > 0 else {
return originalString
}
guard self.numberOfLinesNeeded(forString: originalString) > maxLines else {
return originalString
}
var truncatedString = originalString
var low = originalString.startIndex
var high = originalString.endIndex
// binary search substring
while low != high {
let mid = originalString.index(low, offsetBy: originalString.distance(from: low, to: high)/2)
truncatedString = String(originalString[..<mid])
if self.numberOfLinesNeeded(forString: truncatedString + newEllipsis) <= maxLines {
low = originalString.index(after: mid)
} else {
high = mid
}
}
// substring further to try and truncate at the end of a word
var tempString = truncatedString
var prevLastChar = "a"
for _ in 0..<15 {
if let lastChar = tempString.last {
if (prevLastChar == " " && String(lastChar) != "") || prevLastChar == "." {
truncatedString = tempString
break
}
else {
prevLastChar = String(lastChar)
tempString = String(tempString.dropLast())
}
}
else {
break
}
}
return truncatedString + newEllipsis
}
private func numberOfLinesNeeded(forString string: String) -> Int {
let oneLineHeight = "A".size(withAttributes: [NSAttributedStringKey.font: font]).height
let totalHeight = self.getHeight(forString: string)
let needed = Int(totalHeight / oneLineHeight)
return needed
}
private func getHeight(forString string: String) -> CGFloat {
return string.boundingRect(
with: CGSize(width: self.bounds.size.width, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [NSAttributedStringKey.font: font],
context: nil).height
}
}
ResponsiveLabelは UILabel のサブクラスで、タッチに応答するカスタム トランケーション トークンを追加できます。