SwiftUI Text justified alignment - text

Has anyone found a way to align the string in a Text in a justified way?
In my reading app I'd like to offer the user to choose the justified alignment, where both the .leading both the .trailing alignments are true at the same time. Unfortunately the .multilineTextAlignment modifier for the Text has only three options: .leading, .center and .trailing.
If not I can still wrap an UILabel into a UIViewRepresentable since the UILabel has the .justified alignment option, but I thought it should be available in the Text as well and I was not able to find it.

I know I'm late but I faced same issue , I will write it here, it could help other developer in future
Unfortunately until now Apple doesn't suppourt .justified in SwiftUI 2.0
you need to depend on UIKit
Add this code in your project
struct LabelAlignment: UIViewRepresentable {
var text: String
var textAlignmentStyle : TextAlignmentStyle
var width: CGFloat
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.textAlignment = NSTextAlignment(rawValue: textAlignmentStyle.rawValue)!
label.numberOfLines = 0
label.preferredMaxLayoutWidth = width
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentHuggingPriority(.required, for: .vertical)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
}
}
enum TextAlignmentStyle : Int{
case left = 0 ,center = 1 , right = 2 ,justified = 3 ,natural = 4
}
Then use it like this
LabelAlignment(text: "Your text here", textAlignmentStyle: .justified, width: UIScreen.main.bounds.width - 20)
change 20 to any number you like to add space on left and right
my solution will will depend on text content size
if the text is small the view will be small and vice versa
so it will act as Text()

Related

Draw shape over Text with Modifier in Jetpack Compose

I am trying to obfuscate some text with Jetpack Compose. Obviously, the blur modifier works wonders for this use case if you have Android 12.
My alternative for devices running a lower API version would be to simply draw a rectangle with a black colour over the text. I assumed this would be relatively easy with existing modifiers like graphicsLayer or drawBehind but I haven't been able to figure it out and I'm at a loss for ideas right now...
My current text composable looks like this:
Text(
modifier = if (blurText) {
Modifier.blur(16.dp, BlurredEdgeTreatment.Unbounded)
} else {
Modifier
},
text = textToObfuscate,
fontFamily = latoFontFamily,
fontWeight = FontWeight.W700,
fontSize = 16.sp,
color = black,
)
I could wrap the text in a Box and have another Box inside it to draw over the Text but that just seems useless and more work than should be necessary.
If anyone has any ideas on how to achieve this simply using a Modifier extension, that would be amazing!
You can use Modifier.drawWithContent as
Text(
modifier = if (blurText) {
Modifier.blur(16.dp, BlurredEdgeTreatment.Unbounded)
} else {
Modifier.drawWithContent {
drawContent()
drawRect(Color.Black)
}
},
text = "textToObfuscate",
fontWeight = FontWeight.W700,
fontSize = 16.sp,
)

How to find bit length of text with specific font and font size

I'm developing NativeScript JavaScript code to create dynamic text marker for maps. I have the code working that creates a marker for a specific string. My next step is to take any given string, determine its height and width in bits, and create the marker sized to contain the text.
My problem is finding the size of the text, given the text string itself, the font size, and the font family.
It looks like getMeasuredWidth could work, except that the string must already be loaded on a page before that function will return a value. In my case, I simply need to compute the size; the text won't otherwise appear as such on a page (the text in the marker becomes an image).
Is there a way to do this?
var bmp = BitmapFactory.create(200);
bmp.dispose(function (b) {
try {
b.drawRect(
"100,34", // size
'0,0', // upper-left coordinate
KnownColors.Black, // border color
KnownColors.Cornsilk // fill color
);
b.writeText(
"Parking",
"2,25",
{ color: KnownColors.Black, size: 8, name: 'fontawesome-webfont', });
...
In the code above, the width of "100" of the bounding rectangle actually represents the bit width of "Parking" with a small amount of padding. What I want to does calculate the rectangle's height and width and not hard-code it.
Try this, finding label size without adding it to Page upon button click
export function onFindButtonTap(args: EventData) {
const button = <any>args.object;
const label = new Label();
label.text = "Hello, found my size?"
label.fontSize = 20;
(<any>label)._setupAsRootView(button._context);
label.onLoaded();
label.measure(0, 0);
console.log(`Width : ${label.getMeasuredWidth()} x Height : ${label.getMeasuredHeight()}`);
}
Playground Sample
Note: I didn't get a chance to test it with iOS yet, let me know if you hit any issues.

swift playground UITextField spawns keyboard that is too big

Swift in playground on Mac OS. When the user clicks in a UItextfield, a keyboard spawns but it is very large compared to the view and only the first few keys are available.
minimal example:
import UIKit
import PlaygroundSupport
class TesterViewController : UIViewController {
var testTextField : UITextField!
override func loadView() {
let view = UIView()
view.backgroundColor = .white
testTextField = UITextField()
testTextField.borderStyle = .roundedRect
testTextField.text = ""
view.addSubview(testTextField)
testTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
testTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
testTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
])
self.view = view
}
}
PlaygroundPage.current.liveView = TesterViewController()
screenshot
I face the same issue. It seems as if Playground has a hard-coded screen size of 768x1024 (run UIScreen.main.bounds in the Playground) and shows the keyboard according to this size, independently of the live view's actual size.
The best workaround I came up with is to increase the size of the view controller so that it matches the keyboard:
let vc = TesterViewController()
vc.preferredContentSize = vc.view.frame.size // or a custom CGSize
PlaygroundPage.current.liveView = vc
Of course this makes the view larger than you might want it to be, so I only use this workaround when I really have to access the on-screen keyboard for testing.
System keyboard is presented according to the key window size and in the case of a playground simulator the size is 768x1024. It looks like it is a bug.
A solution which works:
Instead of passing a view controller one should pass a window with a custom size.
let window = UIWindow(frame: CGRect(x: 0,
y: 0,
width: 768,
height: 1024))
let viewController = MyViewController()
window.rootViewController = viewController
window.makeKeyAndVisible()
PlaygroundPage.current.liveView = window
Works in Xcode 11 and above (latest tested version Xcode 12.2)
Edited
I have checked the answer which suggests to use "vc.preferredContentSize" but it doesn't work for me. Tested in Xcode 12.2
let vc = TextFieldViewController()
vc.preferredContentSize = CGSize(width: 768,height: 1024)
PlaygroundPage.current.liveView = vc
In reply to Adobels (Seo 21 '19) ...
I am not sure why he proposed a different approach.
I modified dr_barlo's approach of Oct 22 '17 as follows (and it works in Xcode 11 too):
let vc = TextFieldViewController()
vc.preferredContentSize = CGSize.init(width: 768,height: 1024)
PlaygroundPage.current.liveView = vc
They all appear equivalent in results if not internal workings.

Test for Label overrun

I have a multiline label whose text has a chance of overrunning. If it does this, I want to decrease the font size until it isn't overrunning, or until it hits some minimum size. This would hopefully make it so that the label will change size until the entire string is visible.
My problem it that I am not sure how to test to see if the text has overrun. I have tried testing to see if the label's text ends with the ellipse string, but I believe the ellipse is not technically added to the textProperty of the label. So does anyone know of a good way to test for this?
The short and disappointing answer: You simply cannot do this in a reliable way.
The slightly longer answer is, that the label itself does not even know whether it's overflown or not. Whenever a label is resized, the skin class (LabeledSkinBase) is responsible for updating the displayed text. This class, however, uses a JavaFX utils class to compute the ellipsoided text. The problem here is that the respective method just returns a string that is ellipsoid if this is required by the label's dimensions. The skin itself never gets informed about whether the text was actually ellipsoided or not, it just updates the displayed text to the returned result.
What you could try is to check the displayed text of the skin class, but it's protected. So you would need to do is to subclass LabelSkin, and implement something like that:
package com.sun.javafx.scene.control.skin;
import java.lang.reflect.Field;
import javafx.scene.control.Label;
#SuppressWarnings("restriction")
public class TestLabel extends LabelSkin {
private LabeledText labelledText;
public TestLabel(Label label) throws Exception {
super(label);
for (Field field : LabeledSkinBase.class.getDeclaredFields()) {
if (field.getName().equals("text")) {
field.setAccessible(true);
labelledText = (LabeledText) field.get(this);
break;
}
}
}
public boolean isEllipsoided() {
return labelledText != null && labelledText.getText() != null && !getSkinnable().getText().equals(labelledText.getText());
}
}
If you use this skin for you Label, you should be able to detect whether your text is ellipsoided. If you wonder about the loop and the reflection: Java didn't allow me to access the text field by other means, so this may be a strong indicator that you really should not do this ;-) Nevertheless: It works!
Disclaimer: I've only checked for JavaFX 8
minisu posted a way to detect an overrun in this answer:
https://stackoverflow.com/a/15178908/9492864
This way works for all labeled and I tested it on Buttons with JavaFX 8.
You can add a listener for example to the needsLayoutProperty:
labeled.needsLayoutProperty().addListener((observable, oldValue, newValue) -> {
String originalString = labeled.getText();
Text textNode = (Text) labeled.lookup(".text"); // "text" is the style class of Text
String actualString = textNode.getText();
boolean clipped = !actualString.isEmpty() && !originalString.equals(actualString);
System.out.println("is " + originalString + " clipped: " + clipped);
});
This work for me !!
In javafx
public class LabelHelper{
public static boolean necesitaTooltip(Font font, Label label){
Text text = new Text(label.getText());
text.setFont(font);
Bounds tb = text.getBoundsInLocal();
Rectangle stencil = new Rectangle(
tb.getMinX(), tb.getMinY(), tb.getWidth(), tb.getHeight()
);
Shape intersection = Shape.intersect(text, stencil);
Bounds ib = intersection.getBoundsInLocal();
return ib.getWidth() > label.getPrefWidth();
}
public static void asignarTexto(Label label, String texto){
label.setText(texto);
if (necesitaTooltip(label.getFont(), label)){
Tooltip tp = new Tooltip(texto);
label.setTooltip(tp);
}
}
}
Only call a asignarTexto(label, texto) for set text a label and check if the text is overrun in the label then add a tooltip for label.
It's already been mentioned by Johann that you can use (Text)labeled.lookup(".text") to get the actual displayed text for a Label, then compare it to the intended String... However, in my case, this did not work. Perhaps it was because I was updating the Label with a high frequency, but the actual String was always a few chars less than the intended...
So, I opted to use the setEllipsisString(String value) method to set the ellipsis string (what's appended to the end of a Label when there's overrun, the default being "...") to an (unused)
ASCII control character like 0x03 (appropriately named "end of text"), then after each time I set the Label text I check if the last char of the actual String is the control char.
Example using Platform.runLater(Runnable):
import javafx.scene.control.Label;
import javafx.application.Platform;
import javafx.scene.text.Text;
...
Label label = new Label();
...
label.setEllipsisString("\003");
...
final String newText = "fef foo";
Platform.runLater(() -> {
label.setText(newText);
String actual = ((Text)label.lookup(".text")).getText();
// \003 is octal for the aforementioned "end of text" control char
if (actual.length() > 0 && actual.charAt(actual.length()-1) == '\003') {
// Handle text now that you know it's clipped
}
});
Note that you can set the control char to anything really, and it doesn't need to be just one char; however if you opt for a control character, check that it isn't commonly used.

Resize CATextLayer to fit text on iOS

All my research so far seems to indicate it is not possible to do this accurately. The only two options available to me at the outset were:
a) Using a Layout manager for the CATextLayer - not available on iOS as of 4.0
b) Use sizeWithFont:constrainedToSize:lineBreakMode: and adjust the frame of the CATextLayer according to the size returned here.
Option (b), being the simplest approach, should work. After all, it works perfectly with UILabels. But when I applied the same frame calculation to CATextLayer, the frame was always turning out to be a bit bigger than expected or needed.
As it turns out, the line-spacing in CATextLayers and UILabels (for the same font and size) is different. As a result, sizeWithFont (whose line-spacing calculations would match with that of UILabels) does not return the expected size for CATextLayers.
This is further proven by printing the same text using a UILabel, as against a CATextLayer and comparing the results. The text in the first line overlaps perfectly (it being the same font), but the line-spacing in CATextLayer is just a little shorter than in UILabel. (Sorry I can't upload a screenshot right now as the ones I already have contain confidential data, and I presently don't have the time to make a sample project to get clean screenshots. I'll upload them later for posterity, when I have the time)
This is a weird difference, but I thought it would be possible to adjust the spacing in the CATextLayer by specifying the appropriate attribute for the NSAttributedString I use there, but that does not seem to be the case. Looking into CFStringAttributes.h I can't find a single attribute that could be related to line-spacing.
Bottomline:
So it seems like it's not possible to use CATextLayer on iOS in a scenario where the layer is required to fit to its text. Am I right on this or am I missing something?
P.S:
The reason I wanted to use CATextLayer and NSAttributedString's is because the string to be displayed is to be colored differently at different points. I guess I'd just have to go back to drawing the strings by hand as always....of course there's always the option of hacking the results from sizeWithFont to get the proper line-height.
Abusing the 'code' tags a little to make the post more readable.
I'm not able to tag the post with 'CATextLayer' - surprisingly no such tags exist at the moment. If someone with enough reputation bumps into this post, please tag it accordingly.
Try this:
- (CGFloat)boundingHeightForWidth:(CGFloat)inWidth withAttributedString:(NSAttributedString *)attributedString {
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (CFMutableAttributedStringRef) attributedString);
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(inWidth, CGFLOAT_MAX), NULL);
CFRelease(framesetter);
return suggestedSize.height;
}
You'll have to convert your NSString to NSAttributedString. In-case of CATextLayer, you can use following CATextLayer subclass method:
- (NSAttributedString *)attributedString {
// If string is an attributed string
if ([self.string isKindOfClass:[NSAttributedString class]]) {
return self.string;
}
// Collect required parameters, and construct an attributed string
NSString *string = self.string;
CGColorRef color = self.foregroundColor;
CTFontRef theFont = self.font;
CTTextAlignment alignment;
if ([self.alignmentMode isEqualToString:kCAAlignmentLeft]) {
alignment = kCTLeftTextAlignment;
} else if ([self.alignmentMode isEqualToString:kCAAlignmentRight]) {
alignment = kCTRightTextAlignment;
} else if ([self.alignmentMode isEqualToString:kCAAlignmentCenter]) {
alignment = kCTCenterTextAlignment;
} else if ([self.alignmentMode isEqualToString:kCAAlignmentJustified]) {
alignment = kCTJustifiedTextAlignment;
} else if ([self.alignmentMode isEqualToString:kCAAlignmentNatural]) {
alignment = kCTNaturalTextAlignment;
}
// Process the information to get an attributed string
CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
if (string != nil)
CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), (CFStringRef)string);
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTForegroundColorAttributeName, color);
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTFontAttributeName, theFont);
CTParagraphStyleSetting settings[] = {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTParagraphStyleAttributeName, paragraphStyle);
CFRelease(paragraphStyle);
NSMutableAttributedString *ret = (NSMutableAttributedString *)attrString;
return [ret autorelease];
}
HTH.
I have a much easier solution, that may or may not work for you.
If you aren't doing anything special with the CATextLayer that you can't do a UILabel, instead make a CALayer and add the layer of the UILabel to the CALayer
UILabel*label = [[UILabel alloc]init];
//Do Stuff to label
CALayer *layer = [CALayer layer];
//Set Size/Position
[layer addSublayer:label.layer];
//Do more stuff to layer
With LabelKit you don't need CATextLayer anymore. No more wrong line spacing and wider characters, all is drawn in the same way as UILabel does, while still animated.
This page gave me enough to create a simple centered horizontally CATextLayer : http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html
- (void)drawInContext:(CGContextRef)ctx {
CGFloat height, fontSize;
height = self.bounds.size.height;
fontSize = self.fontSize;
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, 0.0, (fontSize-height)/2.0 * -1.0);
[super drawInContext:ctx];
CGContextRestoreGState(ctx);
}

Resources