Tether uilabel closely to the text being input to UITextField - ios

I want the currency abbreviation uilabel closely follow text being input into UITextField. What's a good way to
calculate where did the text being input ended so
that
func rightViewRect(forBounds bounds: CGRect) -> CGRect
can calculate the label rect properly?

Among other things I've ended up with this helper:
func rightViewRect(bounds: CGRect,
label: UILabel,
field: UITextField
) -> CGRect
{
let measure = UILabel()
measure.font = field.font
if field.text?.isEmpty ?? true {
measure.text = field.placeholder
} else {
measure.text = field.text
}
let cs = measure.intrinsicContentSize
let lcs = label.intrinsicContentSize
guard lcs.width > 0 else {
return .zero
}
let magicSpace = CGFloat(2)
let unclipped = CGRect(x: cs.width + magicSpace, y: 0, width: lcs.width, height: bounds.height)
let clipped = unclipped.intersection(bounds)
return clipped
}

Related

Multiline text label not wrapping in Swift

I'm messing around in a playground trying to get a multi-line UILabel to wrap itself but it won't wrap. I'd expect the label to auto-size itself. Why isn't this working?
I'd prefer not to give it an explicit height and have the label wrap it self
public func Init<Type>(_ value: Type, block: (_ object: Type) -> Void) -> Type {
block(value)
return value
}
let view = Init(UIView()) {
$0.backgroundColor = .white
$0.frame = CGRect(x: 0, y: 0, width: 375, height: 600)
}
let label = Init(UILabel()) {
$0.text = "This is a really long string that wraps to two lines but sometimes three."
$0.textColor = .black
$0.numberOfLines = 0
$0.lineBreakMode = .byWordWrapping
}
struct Style {
static let margin: CGFloat = 12
}
view.addSubview(label)
label.sizeToFit()
label.frame.origin = CGPoint(x: 12, y: 20)
You need to constrain its width, either through it's anchors or explicitly giving it a width.

Get each line of text in a UILabel

I'm trying to add each line in a UILabel to an array, but the code I'm using doesn't appear to be terribly accurate.
func getLinesArrayOfStringInLabel(label:UILabel) -> [String] {
guard let text: NSString = label.text as? NSString else { return [] }
let font:UIFont = label.font
let rect:CGRect = label.frame
let myFont: CTFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil)
let attStr:NSMutableAttributedString = NSMutableAttributedString(string: text as String)
attStr.addAttribute(NSAttributedStringKey.font, value:myFont, range: NSMakeRange(0, attStr.length))
let frameSetter:CTFramesetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
let path: CGMutablePath = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: rect.size.width, height: 100000))
let frame:CTFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
let lines = CTFrameGetLines(frame) as NSArray
var linesArray = [String]()
for line in lines {
let lineRange = CTLineGetStringRange(line as! CTLine)
let range:NSRange = NSMakeRange(lineRange.location, lineRange.length)
let lineString = text.substring(with: range)
linesArray.append(lineString as String)
}
return linesArray
}
let label = UILabel()
label.numberOfLines = 0
label.frame = CGRect(x: 40, y: 237, width: 265, height: 53)
label.font = UIFont.systemFont(ofSize: 22, weight: UIFont.Weight.regular)
label.text = "Hey there how's it going today?"
label.backgroundColor = .red
bg.addSubview(label)
print(getLinesArrayOfStringInLabel(label: label))
This prints
["Hey there how\'s it going ", "today?"]
But the label looks like this:
I expected to get ["Hey there how\'s it ", "going today?"]. What's going on?
So it appears to be something with UILabel and not something wrong with the function you are using. It was my suspicion that a CATextLayer would render the lines how they are returned from that method and I found out sadly :( that I am right.
Here is a picture of my results:
The red is the exact code you used to create your UILabel.
The green is a CATextLayer with all of the same characteristics of the UILabel from above including font, fontsize, and frame size.
The yellow is a subclassed UIView that is replacing its own layer and returning a CATextLayer. I am attaching it below. You can continue to build it out to meet your needs but I think this is the real solution and the only one that will have the get lines matching the visible lines the user sees. If you come up with a better solution please let me know.
import UIKit
class AGLabel: UIView {
var alignment : String = kCAAlignmentLeft{
didSet{
configureText()
}
}
var font : UIFont = UIFont.systemFont(ofSize: 16){
didSet{
configureText()
}
}
var fontSize : CGFloat = 16.0{
didSet{
configureText()
}
}
var textColor : UIColor = UIColor.black{
didSet{
configureText()
}
}
var text : String = ""{
didSet{
configureText()
}
}
override class var layerClass: AnyClass {
get {
return CATextLayer.self
}
}
func configureText(){
if let textLayer = self.layer as? CATextLayer{
textLayer.foregroundColor = textColor.cgColor
textLayer.font = font
textLayer.fontSize = fontSize
textLayer.string = text
textLayer.contentsScale = UIScreen.main.scale
textLayer.contentsGravity = kCAGravityCenter
textLayer.isWrapped = true
}
}
}
You should also check out Core-Text-Label on GitHub. It renders exactly as the CATextLayers do and would match the return of the get lines. It won't work for my particular needs as I need mine to be resizable and it crashes but if resizing is not need then I would check it out.
Finally I am back again and it appears that it could be a problem of word wrap that was started in iOS 11 where they do not leave an orphan word on a line.

How to calculate the optimal label width for multiline text in swift

I'd like to create a method to calculate the optimal width of a multi-line label to attach several labels in a horizontal row of a fixed height.
With one line of text there is no problem:
let textAttributes: [String : Any] = [NSFontAttributeName: UIFont.preferredFont(forTextStyle: UIFontTextStyle.title2)]
let maximalWidth: CGFloat = text!.boundingRect(
with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: height),
options: [NSStringDrawingOptions.usesLineFragmentOrigin],
attributes: textAttributes,
context: nil).size.width
As far as I understood, there is no option to indicate here, that I have several lines. This method works well in other direction when we calculate the height of the text with the fixed width. But I have the opposite goal.
As a variant, I can create a label based on the longest word (to be more precise, based on the widest word, as we can have several words with the same characters count, but different rendered width):
var sizeToReturn = CGSize()
let maxWordsCharacterCount = text?.maxWord.characters.count
let allLongWords: [String] = text!.wordList.filter {$0.characters.count == maxWordsCharacterCount}
var sizes: [CGFloat] = []
allLongWords.forEach {sizes.append($0.size(attributes: attributes).width)}
let minimalWidth = (sizes.max()! + constantElementsWidth)
I used here two String extensions to create words list and find all longest:
extension String {
var wordList: [String] {
return Array(Set(components(separatedBy: .punctuationCharacters).joined(separator: "").components(separatedBy: " "))).filter {$0.characters.count > 0}
}
}
extension String {
var maxWord: String {
if let max = self.wordList.max(by: {$1.characters.count > $0.characters.count}) {
return max
} else {return ""}
}
}
Not a bad option, but it looks ugly if we have the text that can't be fitted in three lines and that has several short words and one long word at the end. This long word, determined the width, will be just truncated. And more of that it looks not too good with 3 short words like:
Sell
the
car
Well, I have the minimum width, I have the maximum width. Perhaps, I can
go from maximum to minimum and catch when the label starts being truncated.
So I feel that there can be an elegant solution, but I'm stuck.
Hooray, I've found one of the possible solutions. You can use the code below in the playground:
import UIKit
import PlaygroundSupport
//: Just a view to launch playground timeline preview
let hostView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
hostView.backgroundColor = .lightGray
PlaygroundPage.current.liveView = hostView
// MARK: - Extensions
extension String {
var wordList: [String] {
return Array(Set(components(separatedBy: .punctuationCharacters).joined(separator: "").components(separatedBy: " "))).filter {$0.characters.count > 0}
}
}
extension String {
var longestWord: String {
if let max = self.wordList.max(by: {$1.characters.count > $0.characters.count}) {
return max
} else {return ""}
}
}
// MARK: - Mathod
func createLabelWithOptimalLabelWidth (
requestedHeight: CGFloat,
constantElementsWidth: CGFloat,
acceptableWidthForTextOfOneLine: CGFloat, //When we don't want the text to be shrinked
text: String,
attributes: [String:Any]
) -> UILabel {
let label = UILabel(frame: .zero)
label.attributedText = NSAttributedString(string: text, attributes: attributes)
let maximalLabelWidth = label.intrinsicContentSize.width
if maximalLabelWidth < acceptableWidthForTextOfOneLine {
label.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: maximalLabelWidth, height: requestedHeight))
return label // We can go with this width
}
// Minimal width, calculated based on the longest word
let maxWordsCharacterCount = label.text!.longestWord.characters.count
let allLongWords: [String] = label.text!.wordList.filter {$0.characters.count == maxWordsCharacterCount}
var sizes: [CGFloat] = []
allLongWords.forEach {sizes.append($0.size(attributes: attributes).width)}
let minimalWidth = (sizes.max()! + constantElementsWidth)
// Height calculation
var flexibleWidth = maximalLabelWidth
var flexibleHeight = CGFloat()
var optimalWidth = CGFloat()
var optimalHeight = CGFloat()
while (flexibleHeight <= requestedHeight && flexibleWidth >= minimalWidth) {
optimalWidth = flexibleWidth
optimalHeight = flexibleHeight
flexibleWidth -= 1
flexibleHeight = label.attributedText!.boundingRect(
with: CGSize(width: flexibleWidth, height: CGFloat.greatestFiniteMagnitude),
options: [NSStringDrawingOptions.usesLineFragmentOrigin],
context: nil).size.height
print("Width: \(flexibleWidth)")
print("Height: \(flexibleHeight)")
print("_______________________")
}
print("Final Width: \(optimalWidth)")
print("Final Height: \(optimalHeight)")
label.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: optimalWidth+constantElementsWidth, height: requestedHeight))
return label
}
// MARK: - Inputs
let text: String? = "Determine the fair price"//nil//"Select the appropriate payment method"//"Finalize the order" //"Sell the car"//"Check the payment method"
let font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.callout)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.allowsDefaultTighteningForTruncation = true
let attributes: [String:Any] = [
NSFontAttributeName: font,
NSParagraphStyleAttributeName: paragraphStyle,
NSBaselineOffsetAttributeName: 0
]
if text != nil {
let label = createLabelWithOptimalLabelWidth(requestedHeight: 70, constantElementsWidth: 0, acceptableWidthForTextOfOneLine: 120, text: text!, attributes: attributes)
label.frame.width
label.frame.height
label.backgroundColor = .white
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 3
hostView.addSubview(label)
}

ios: how to start cursor at specific position?

I want cursor at specific position.
I show my requirement in image.
let arbitraryValue: Int = 5
if let newPosition = txtroutine.position(from: txtroutine.beginningOfDocument, offset: arbitraryValue) {
txtroutine.selectedTextRange = txtroutine.textRange(from: newPosition, to: newPosition)
}
I have something like this but my code is in obj-c hope you can make it in swift,
Get the current position of cursor
- (NSInteger)cursorPosition
{
UITextRange *selectedRange = self.selectedTextRange;
UITextPosition *textPosition = selectedRange.start;
return [self offsetFromPosition:self.beginningOfDocument toPosition:textPosition];
}
// set cursor at your specfic location
- (void)setCursorPosition:(NSInteger)position
{
UITextPosition *textPosition = [self positionFromPosition:self.beginningOfDocument offset:position];
[self setSelectedTextRange:[self textRangeFromPosition:textPosition toPosition:textPosition]];
}
You can achieve this by overriding -textRectForBounds:. It will only change the inset of text i.e in your case it's cursor.
You need to subclass UITextField for that.
class PaddedTextfield: UITextField {
var horizontalInsetValue: CGFloat = 0
var verticalInsetValue: CGFloat = 0
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: horizontalInsetValue, dy: verticalInsetValue)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: horizontalInsetValue , dy: verticalInsetValue)
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: horizontalInsetValue, dy: verticalInsetValue)
}
}
You can use this textfield wherever you want.
you can use leftView property of UITextField to set position as per your requirement :
//add a 12pt padding to the textField
Swift 3.0 :
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 0))
textField.leftViewMode = .always
Try this , feel free to comment .
my problem is solved by adding this simple line in viewdidload()
self.txtroutine.layer.sublayerTransform = CATransform3DMakeTranslation(15, 0, 0);

How can I build a Matrix of textFields with dynamic cols&rows

At very beginning, I want to just create every textField by my self-design function:
func creatTextField(x x0:Int,y y0: Int,w w0: Int,h h0: Int) -> UITextField{
let x = CGFloat(x0)
let y = CGFloat(y0)
let w = CGFloat(w0)
let h = CGFloat(h0)
let frame = CGRectMake(0, 0, w, h)
let center = CGPointMake(x/2, y/2)
let tf = UITextField(frame: frame)
tf.center = center
view.addSubview(tf)
return tf
}
and In the viewDidLoad(),I just want to add a new TextField:
override func viewDidLoad() {
super.viewDidLoad()
TextField = creatTextField(x: 50, y: 50, w: 100, h: 30)
}
And nothing changed when I ran the program
I am considering that did I miss something such as CONSTRAIN or I should use STACK VIEW to present those textFields correctly?
I hope someone can help me out! :D
b.t.w.
Because it just a test, I just add one textField.But I should be a 2D-Array< UITextField >
Just add this line into your code:
tf.borderStyle = UITextBorderStyle.RoundedRect
And final code will be:
func creatTextField(x x0:Int,y y0: Int,w w0: Int,h h0: Int) -> UITextField{
let x = CGFloat(x0)
let y = CGFloat(y0)
let w = CGFloat(w0)
let h = CGFloat(h0)
let frame = CGRectMake(0, 0, w, h)
let center = CGPointMake(x/2, y/2)
let tf = UITextField(frame: frame)
tf.center = center
tf.borderStyle = UITextBorderStyle.RoundedRect
view.addSubview(tf)
return tf
}

Resources