TextAlignmentStyle .natural doesn't work with SwiftUI - swift

I need my text to change depend on text language not depend on App language
if it's RTL language like Arabic the text alignment should be RTL
if it's LTR language like English the text alignment should be LTR
I tried to import UILabel from UIKit because SwiftUI doesn't have .natural and .justified
like this
struct LabelAlignment: UIViewRepresentable {
var text: String
var textAlignmentStyle : TextAlignmentStyle
var width: CGFloat
var fontSize: CGFloat
var textColorName: String
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)
label.textColor = UIColor(named: textColorName)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
if let customFont = UIFont(name: "Almarai-Regular", size: fontSize) {
uiView.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: customFont)
uiView.adjustsFontForContentSizeCategory = true
} }
}
enum TextAlignmentStyle : Int{
case left = 0 ,center = 1 , right = 2 ,justified = 3 ,natural = 4
}
then I use it like this
HStack {
LabelAlignment(text: "My text" , textAlignmentStyle: . natural, width:UIScreen.main.bounds.width - 30, fontSize: 15, textColorName: "color3")
.padding(.horizontal,5)
.layoutPriority(1.0)
.foregroundColor(.textColor3)
.regularStyle(size: 15)
.padding([.bottom,.top], 8)
}
it working great with left ,center , right and justified
but it won't work with natural

As Adam mention
if you follow my old answer it won't work correctly if you change the App language
The correct way is
struct LabelAlignment: UIViewRepresentable {
var text: String
var width: CGFloat
var fontSize: CGFloat
var textColorName: String
var detectLangauge = ""
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
if detectLangauge == "Arabic" || detectLangauge == "العربية" {
label.textAlignment = .right
}else {
label.textAlignment = .left
}
label.numberOfLines = 0
label.preferredMaxLayoutWidth = width
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentHuggingPriority(.required, for: .vertical)
label.textColor = UIColor(named: textColorName)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
if let customFont = UIFont(name: "Almarai-Regular", size: fontSize) {
uiView.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: customFont)
uiView.adjustsFontForContentSizeCategory = true
} }
}
Then use it like this
LabelAlignment(text: "My text", width: UIScreen.main.bounds.width - 30, fontSize: 15, textColorName: "color3",detectLangauge:detectLangauge)
it will work as .natural
Old answer :
I found a solution with pure SwiftUI
First you need to add these code
import Foundation
import NaturalLanguage
extension String {
func detectedLanguage() -> String? {
let recognizer = NLLanguageRecognizer()
recognizer.processString(self)
guard let languageCode = recognizer.dominantLanguage?.rawValue else { return nil }
let detectedLanguage = Locale.current.localizedString(forIdentifier: languageCode)
return detectedLanguage
}
}
then you need a #State value to save the result of detectedLanguage()
#State private var detectLangauge = ""
after that check the result of text by using
detectLangauge = "your text".detectedLanguage()
you can check your text language onApper() or after get result from API
then you need to add
Text("Your Text")
.multilineTextAlignment(detectLangauge == "Arabic" ? .trailing : .leading)

Related

SwiftUI Line break is Weird [duplicate]

I have a text field like this
Text("Hello, one two three four five six seven eight!")
.frame(width:270)
.border(.blue)
When it renders it decides to put seven and eight on the second line even though there is space for seven on the first line. Worse it decides to indent the truncated top line so it is centred within the frame.
How do I fix this so it wraps the text properly without taking into account the orphan?
Edit: Forgot to mention that I wanted this on macOS. I have tried to port it to the Mac. It does correctly left align the text but it doesn't wrap to the second line. The height of the box does get calculated accordingly though.
Here is my updated code:
struct NonOrphanedText: View
{
var text: String
#State private var height: CGFloat = .zero
var body: some View
{
InternalLabelView(text: text, dynamicHeight: $height)
.frame(maxHeight: height)
}
struct InternalLabelView: NSViewRepresentable
{
var text: String
#Binding var dynamicHeight: CGFloat
func makeNSView(context: Context) -> NSTextField
{
let label = NSTextField()
label.isEditable = false
label.isBezeled = false
label.drawsBackground = false
label.isSelectable = false
label.maximumNumberOfLines = 5
label.usesSingleLineMode = false
label.lineBreakStrategy = .init()
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}
func updateNSView(_ nsView: NSTextField, context: Context)
{
nsView.stringValue = text
DispatchQueue.main.async
{
dynamicHeight = nsView.sizeThatFits(CGSize(width: nsView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}
We need lineBreakStrategy but it is unavailable for now in SwiftUI, so possible solution is to use UILabel.
Here is a possible solution. Tested with Xcode 13.2 / iOS 15.2
struct LabelView: View {
var text: String
#State private var height: CGFloat = .zero
var body: some View {
InternalLabelView(text: text, dynamicHeight: $height)
.frame(maxHeight: height)
}
struct InternalLabelView: UIViewRepresentable {
var text: String
#Binding var dynamicHeight: CGFloat
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakStrategy = .init() // << here !!
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
DispatchQueue.main.async {
dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}

Why UILabelViewRepresentable does not respect to given frame?

I have a simple UILabelViewRepresentable and I gave a frame to it! But after using in SwiftUI it became max size! How could I solve the issue from UIKit code part?
struct ContentView: View {
var body: some View {
UILabelViewRepresentable(configuration: { label in
label.text = "Hello, World!"
label.textAlignment = .center
label.backgroundColor = .blue
label.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
})
}
}
struct UILabelViewRepresentable: UIViewRepresentable {
let configuration: (UILabel) -> ()
func makeUIView(context: Context) -> UILabel {
return UILabel()
}
func updateUIView(_ uiView: UILabel, context: Context) {
configuration(uiView)
}
}
The main issue is that this representable will try constrain the UILabel to fill the whole screen. You can avoid this by making a UIView, and making the UILabel a child of that so you can center it in the parent.
Code:
struct UILabelViewRepresentable: UIViewRepresentable {
let configuration: (UILabel) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.addSubview(UILabel())
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
let label = uiView.subviews.first! as! UILabel
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: uiView.centerXAnchor),
label.centerYAnchor.constraint(equalTo: uiView.centerYAnchor)
])
configuration(label)
}
}
Usage:
struct ContentView: View {
var body: some View {
UILabelViewRepresentable(configuration: { label in
label.text = "Hello, World!"
label.textAlignment = .center
label.backgroundColor = .blue
label.sizeToFit()
})
}
}
Result:
Depending on your usage, the label.sizeToFit() can be moved inside the UILabelViewRepresentable.
struct ContentView: View {
var body: some View {
UILabelViewRepresentable(text: "Hello, World!",
center: .center,
frame: CGRect(x: 0,
y: 0,
width: 200,
height: 50),
background: .blue)
}
}
struct UILabelViewRepresentable: UIViewRepresentable {
let text : String
let center: NSTextAlignment
let frame: CGRect
let background: UIColor
func makeUIView(context: Context) -> UIView {
let view = UIView()
let label = UILabel()
label.text = text
label.textAlignment = center
label.backgroundColor = background
label.frame = frame
view.addSubview(label)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}

SwiftUI Breaking line in UITextView wrapped by UIViewRepresentable

I'm struggling for breaking line in CustomUITextView. Please help...
I made CustomUITextView like below.
struct CustomTextView: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<CustomTextView>) -> UITextView {
let textView = UITextView()
textView.backgroundColor = UIColor.clear
textView.isScrollEnabled = false
textView.textColor = UIColor.black
textView.font = UIFont(name: "ArialMT", size: 20)
// textView.text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) { }
}
Then using it like this.
struct ContentView: View {
var body: some View {
ZStack {
CustomTextView()
.frame(width:300, height: 300, alignment: .topLeading)
.border(Color.red, width: 1)
}
}
}
By keyboard input it works and lines are returned at red border.
But when I take away this comment in CustomUITextView
textView.text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
It will be like this and doesn't break.
Is it possible to break line when I put Strings directly from code like above too ?
Does anyone know the solutions?
You need to decrease compression resistance priority
textView.font = UIFont(name: "ArialMT", size: 20)
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
return textView

How to change a variable UITextView after clicking the button?

I create UITextView with a random tag and text, but it is created with one variable, is it possible to update the variable after creation UITextView (by clicking the add button)? Maybe add a random number to it, for example newText1, newText2.. etc.
So that the next UITextView is already created with a new variable?
P.S Sorry, if the question is silly, I just recently started to study Swift
#IBOutlet weak var addTextButton: UIButton!
#IBOutlet weak var StoriesView: UIView!
var newText = UITextView()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addTextButton(_ sender: Any) {
let maxNumber = 10000
let i = Int(arc4random_uniform(UInt32(maxNumber)))
newText = UITextView(frame: CGRect(x: self.StoriesView.frame.origin.x + 40, y: self.StoriesView.frame.origin.y + 40, width: 380, height: 80))
self.StoriesView.addSubview(newText)
newText.font = UIFont(name: "Verdana", size: 11)
newText.text = "TAP TO EDIT #\(i)"
newText.sizeToFit()
newText.textColor = UIColor.black
newText.backgroundColor = UIColor.clear
newText.tag = i
newText.isEditable = true
newText.isSelectable = true
newText.isUserInteractionEnabled = true
newText.allowsEditingTextAttributes = true
newText.translatesAutoresizingMaskIntoConstraints = true
newText.enablesReturnKeyAutomatically = true
newText.delegate = self
}
UPD:
let fontToolbar = UIToolbar(frame:CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
fontToolbar.barStyle = .default
fontToolbar.items = [
UIBarButtonItem(title: "Green", style: .plain, target: self, action: #selector(greenColor)),
UIBarButtonItem(title: "Blue", style: .plain, target: self, action: #selector(blueColor)),
UIBarButtonItem(title: "Red", style: .plain, target: self, action: #selector(redColor)),
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
UIBarButtonItem(title: "Close Keyboard", style: .plain, target: self, action: #selector(dismissKeyboard))]
fontToolbar.sizeToFit()
newText.inputAccessoryView = fontToolbar
in the toolBar above the keyboard I have buttons, here we change the color
#objc func redColor() {
newText.textColor = UIColor.red}
#objc func blueColor() {
newText.textColor = UIColor.blue}
#objc func greenColor() {
newText.textColor = UIColor.green}
So the color changes only in the newly created UITextView
On click of button, create a new texView and assign it a tag value. Once it is added, update the value of i to +1, so that every textView added has a new tag value.
var i = 1
var newText = UITextView()
#IBAction func addTextButton(_ sender: Any) {
newText = UITextView(frame: CGRect(x: self.StoriesView.frame.origin.x + 40, y: self.StoriesView.frame.origin.y + 40, width: 380, height: 80))
self.StoriesView.addSubview(newText)
newText.font = UIFont(name: "Verdana", size: 11)
newText.text = "TAP TO EDIT #\(i)"
newText.sizeToFit()
newText.textColor = UIColor.black
newText.backgroundColor = UIColor.clear
newText.tag = i
newText.isEditable = true
newText.isSelectable = true
newText.isUserInteractionEnabled = true
newText.allowsEditingTextAttributes = true
newText.translatesAutoresizingMaskIntoConstraints = true
newText.enablesReturnKeyAutomatically = true
newText.delegate = self
//increment i
i+=1
}
then you can access your textField via tag values like this:
if let textView = self.StoriesView.viewWithTag(i) as? UITextView {
// textView.text = "change it"
}
UPDATE:
Add textView Delegate method, and once a textView starts editing, change the newText value to the currently editing textView
class ViewController : UIViewController, UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
newText = textView
}
}
I have modified your code a bit to have new UITextView object with button click
import UIKit
class ScannerViewController: UIViewController, UITextViewDelegate {
#IBOutlet weak var StoriesView: UIView!
#IBOutlet weak var addTextButton: UIButton!
var yposition: CGFloat!
var textFieldTag: [Int]! = []
override func viewDidLoad() {
super.viewDidLoad()
yposition = 20
}
#IBAction func addTextButton(_ sender: Any) {
let xposition = self.StoriesView.frame.origin.x
let maxNumber = 10000
let i = Int(arc4random_uniform(UInt32(maxNumber)))
textFieldTag.append(i)
let newText = UITextView(frame: CGRect(x: xposition , y: yposition , width: 380, height: 40))
self.StoriesView.addSubview(newText)
newText.font = UIFont(name: "Verdana", size: 11)
newText.text = "TAP TO EDIT #\(i)"
newText.sizeToFit()
newText.textColor = UIColor.black
newText.backgroundColor = UIColor.yellow
newText.tag = i
newText.isEditable = true
newText.isSelectable = true
newText.isUserInteractionEnabled = true
newText.allowsEditingTextAttributes = true
newText.translatesAutoresizingMaskIntoConstraints = true
newText.enablesReturnKeyAutomatically = true
newText.delegate = self
yposition = yposition + 45
}
#IBAction func accessTextFields(_ sender: Any) {
//access all text fields
for tag in textFieldTag {
if let textField = self.StoriesView.viewWithTag(tag) as? UITextView {
//change properties here
textField.backgroundColor = .cyan
}
}
//access specific text fields
if let textField = self.StoriesView.viewWithTag(textFieldTag.first!) as? UITextView {
//change properties here
textField.backgroundColor = .orange
}
if let textField = self.StoriesView.viewWithTag(textFieldTag[textFieldTag.count - 1]) as? UITextView {
//change properties here
textField.backgroundColor = .green
}
}
}
It will have an output as this!!

Create UIButtons with dynamic font size but all share same font size in UIStackView

I am using UIStackView and adding three buttons to it. I want it so that the button with the most text (B1) will be auto resized to fit the width and the other buttons will share the same font size as B1.
#IBOutlet weak var stackView: UIStackView!
var btnTitles = [String]()
btnTitles.append("Practice Exams")
btnTitles.append("Test Taking Tips")
btnTitles.append("About")
createButtons(buttonTitles: btnTitles)
var min = CGFloat(Int.max) // keep track of min font
func createButtons(buttonTitles: [String]) {
var Buttons = [UIButton]()
for title in buttonTitles {
let button = makeButtonWithText(text: title)
// set the font to dynamically size
button.titleLabel!.numberOfLines = 1
button.titleLabel!.adjustsFontSizeToFitWidth = true
button.titleLabel!.baselineAdjustment = .alignCenters // I think it keeps it centered vertically
button.contentEdgeInsets = UIEdgeInsetsMake(5, 10, 5, 10); // set margins
if (button.titleLabel?.font.pointSize)! < min {
min = (button.titleLabel?.font.pointSize)! // to get the minimum font size of any of the buttons
}
stackView.addArrangedSubview(button)
Buttons.append(button)
}
}
func makeButtonWithText(text:String) -> UIButton {
var myButton = UIButton(type: UIButtonType.system)
//Set a frame for the button. Ignored in AutoLayout/ Stack Views
myButton.frame = CGRect(x: 30, y: 30, width: 150, height: 100)
// background color - light blue
myButton.backgroundColor = UIColor(red: 0.255, green: 0.561, blue: 0.847, alpha: 1)
//State dependent properties title and title color
myButton.setTitle(text, for: UIControlState.normal)
myButton.setTitleColor(UIColor.white, for: UIControlState.normal)
// set the font to dynamically size
myButton.titleLabel!.font = myButton.titleLabel!.font.withSize(70)
myButton.contentHorizontalAlignment = .center // align center
return myButton
}
I wanted to find the minimum font size and then set all the buttons to the minimum in viewDidAppear button the font prints as 70 for all of them even though they clearly appear different sizes (see image)
override func viewDidAppear(_ animated: Bool) {
print("viewDidAppear")
let button = stackView.arrangedSubviews[0] as! UIButton
print(button.titleLabel?.font.pointSize)
let button1 = stackView.arrangedSubviews[1] as! UIButton
print(button1.titleLabel?.font.pointSize)
let button2 = stackView.arrangedSubviews[2] as! UIButton
print(button2.titleLabel?.font.pointSize)
}
image
You can try playing around with this func to return the scaled-font-size of a label:
func actualFontSize(for aLabel: UILabel) -> CGFloat {
// label must have text, must have .minimumScaleFactor and must have .adjustsFontSizeToFitWidth == true
guard let str = aLabel.text,
aLabel.minimumScaleFactor > 0.0,
aLabel.adjustsFontSizeToFitWidth
else { return aLabel.font.pointSize }
let attributes = [NSAttributedString.Key.font : aLabel.font]
let attStr = NSMutableAttributedString(string:str, attributes:attributes as [NSAttributedString.Key : Any])
let context = NSStringDrawingContext()
context.minimumScaleFactor = aLabel.minimumScaleFactor
_ = attStr.boundingRect(with: aLabel.bounds.size, options: .usesLineFragmentOrigin, context: context)
return aLabel.font.pointSize * context.actualScaleFactor
}
On viewDidAppear() you would loop through the buttons, getting the smallest actual font size, then set the font size for each button to that value.
It will take some experimentation... For one thing, I've noticed in the past that font-sizes can get rounded - so setting a label's font point size to 20.123456789 won't necessarily give you that exact point size. Also, since this changes the actual font size assigned to the labels, you'll need to do some resetting if you change the button title dynamically. Probably also need to account for button frame changes (such as with device rotation, etc).
But... here is a quick test that you can run to see the approach:
class TestViewController: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .center
v.distribution = .fillEqually
v.spacing = 8
return v
}()
var btnTitles = [String]()
var theButtons = [UIButton]()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fixButtonFonts()
}
func setupUI() -> Void {
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40),
])
btnTitles.append("Practice Exams")
btnTitles.append("Test Taking Tips")
btnTitles.append("About")
createButtons(buttonTitles: btnTitles)
}
func fixButtonFonts() -> Void {
var minActual = CGFloat(70)
// get the smallest actual font size
theButtons.forEach { btn in
if let lbl = btn.titleLabel {
let act = actualFontSize(for: lbl)
// for debugging
//print("actual font size: \(act)")
minActual = Swift.min(minActual, act)
}
}
// set font size for each button
theButtons.forEach { btn in
if let lbl = btn.titleLabel {
lbl.font = lbl.font.withSize(minActual)
}
}
}
func createButtons(buttonTitles: [String]) {
for title in buttonTitles {
let button = makeButtonWithText(text: title)
// set the font to dynamically size
button.titleLabel!.numberOfLines = 1
button.titleLabel!.adjustsFontSizeToFitWidth = true
// .minimumScaleFactor is required
button.titleLabel!.minimumScaleFactor = 0.05
button.titleLabel!.baselineAdjustment = .alignCenters // I think it keeps it centered vertically
button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10); // set margins
stackView.addArrangedSubview(button)
theButtons.append(button)
}
}
func makeButtonWithText(text:String) -> UIButton {
let myButton = UIButton(type: UIButton.ButtonType.system)
//Set a frame for the button. Ignored in AutoLayout/ Stack Views
myButton.frame = CGRect(x: 30, y: 30, width: 150, height: 100)
// background color - light blue
myButton.backgroundColor = UIColor(red: 0.255, green: 0.561, blue: 0.847, alpha: 1)
//State dependent properties title and title color
myButton.setTitle(text, for: UIControl.State.normal)
myButton.setTitleColor(UIColor.white, for: UIControl.State.normal)
// set the font to dynamically size
myButton.titleLabel!.font = myButton.titleLabel!.font.withSize(70)
myButton.contentHorizontalAlignment = .center // align center
return myButton
}
func actualFontSize(for aLabel: UILabel) -> CGFloat {
// label must have text, must have .minimumScaleFactor and must have .adjustsFontSizeToFitWidth == true
guard let str = aLabel.text,
aLabel.minimumScaleFactor > 0.0,
aLabel.adjustsFontSizeToFitWidth
else { return aLabel.font.pointSize }
let attributes = [NSAttributedString.Key.font : aLabel.font]
let attStr = NSMutableAttributedString(string:str, attributes:attributes as [NSAttributedString.Key : Any])
let context = NSStringDrawingContext()
context.minimumScaleFactor = aLabel.minimumScaleFactor
_ = attStr.boundingRect(with: aLabel.bounds.size, options: .usesLineFragmentOrigin, context: context)
return aLabel.font.pointSize * context.actualScaleFactor
}
}
Result: