iOS: Remove default drawing of UITextField - ios

I'm trying to create a custom text field with a suffix, and I override the draw(CGRect) method to do this. I want both the text and the suffix to align center. Calculating and drawing them works as I want, however, the default text is still there and it overlaps with my newly drawn texts. So I want to completely remove the default drawing of UITextField.
Here is my implementation:
class SuffixTextField: UITextField {
private let suffix: String
private let suffixAttributes: [NSAttributedString.Key : Any]
private let spacing: CGFloat
/// Create a text field with suffix text
/// - Parameters:
/// - suffix: The suffix text
/// - suffixAttributes: Attributes to apply to the suffix
/// - spacing: Spacing between the content and the suffix
init(suffix: String, suffixAttributes: [NSAttributedString.Key : Any], spacing: CGFloat) {
self.suffix = suffix
self.suffixAttributes = suffixAttributes
self.spacing = spacing
super.init(frame: .zero)
addTarget(self,
action: #selector(textFieldDidChange),
for: UIControl.Event.editingChanged)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
guard !suffix.isEmpty else {
super.draw(rect)
return
}
let text = (self.text ?? "") as NSString
let textSize = text.size(withAttributes: typingAttributes)
let fieldSize = frame.size
let suffixSize = (suffix as NSString).size(withAttributes: suffixAttributes)
func drawSuffix(xPosition: CGFloat) {
let suffixYPosition = (fieldSize.height / 2) - (suffixSize.height / 2)
let rect = CGRect(origin: .init(x: xPosition, y: suffixYPosition),
size: suffixSize)
(suffix as NSString).draw(in: rect, withAttributes: suffixAttributes)
}
switch textAlignment {
case .left:
super.draw(rect)
drawSuffix(xPosition: textSize.width + spacing)
case .center:
let textXPosition = (fieldSize.width - textSize.width - spacing - suffixSize.width) / 2
let textYPosition = (fieldSize.height - textSize.height) / 2
text.draw(in: CGRect(origin: .init(x: textXPosition, y: textYPosition), size: textSize),
withAttributes: typingAttributes)
let suffixXPosition = textXPosition + textSize.width + spacing
drawSuffix(xPosition: suffixXPosition)
default:
fatalError("Cannot handle other allignment, please implement here")
}
}
#objc private func textFieldDidChange() {
setNeedsDisplay()
}
}

You don't need to call super.draw(rect) and draw your own text as textfield will always draw its text by itself. What you can do is that you can place your suffix text accordingly and it will work.
override func draw(_ rect: CGRect) {
guard !suffix.isEmpty else {
return
}
let text = (self.text ?? "") as NSString
let textSize = text.size(withAttributes: typingAttributes)
let fieldSize = frame.size
let suffixSize = (suffix as NSString).size(withAttributes: suffixAttributes)
func drawSuffix(xPosition: CGFloat) {
let suffixYPosition = (fieldSize.height / 2) - (suffixSize.height / 2)
let rect = CGRect(origin: .init(x: xPosition, y: suffixYPosition),
size: suffixSize)
(suffix as NSString).draw(in: rect, withAttributes: suffixAttributes)
}
switch textAlignment {
case .left:
drawSuffix(xPosition: textSize.width + spacing)
case .center:
let textXPosition = (fieldSize.width - textSize.width - spacing - suffixSize.width) / 2
let suffixXPosition = textXPosition + textSize.width + spacing
drawSuffix(xPosition: suffixXPosition)
default:
fatalError("Cannot handle other allignment, please implement here")
}
}

Related

Detect clicked mutable attributed string

I have this code to write a paragraph like book with numbering for each sentence , the problem I'm facing is i can't find how to color one sentence when the user clicks in any word from it
import UIKit
let descender: CGFloat = UIFont.systemFont(ofSize: 25).descender
class ViewController: UIViewController , UITextViewDelegate, UIGestureRecognizerDelegate {
var all = [NSMutableAttributedString]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let style = NSMutableParagraphStyle()
style.alignment = NSTextAlignment.justified
style.baseWritingDirection = .rightToLeft
style.lineBreakMode = .byWordWrapping
let myAttribute = [ NSAttributedString.Key.font: UIFont.systemFont(ofSize: 25)] // ,
// NSAttributedString.Key.paragraphStyle: style ,
// NSAttributedString.Key.baselineOffset: NSNumber(value: 0)]
let textView = UITextView(frame:CGRect(x: 20, y: 100, width: UIScreen.main.bounds.width - 40 , height: UIScreen.main.bounds.height))
let attributedString = NSMutableAttributedString()
Array(1..<50).forEach {
let small = $0 % 2 == 0 ? " long text part one long text part one long text part one long text part one long text part one long text part one long text part one long text part one long text part one " : "long text part two long text part twolong text part twolong text part twolong text part twolong text part twolong text part twolong text part two "
let attributedString2 = NSMutableAttributedString(string: small,attributes: myAttribute)
attributedString.append(attributedString2)
let textAttachment11 = SubTextAttachment()
textAttachment11.image = generateImageWithText(text: "\($0)")
let attrStringWithImage11 = NSAttributedString(attachment: textAttachment11)
attributedString.append(attrStringWithImage11)
}
textView.attributedText = attributedString;
self.view.addSubview(textView)
textView.isEditable = false
textView.isSelectable = true
textView.delegate = self
let tap = UITapGestureRecognizer(target: self, action: #selector(self.textTapped(_:)))
tap.delegate = self
textView.isUserInteractionEnabled = true
textView.addGestureRecognizer(tap)
}
func generateImageWithText(text: String) -> UIImage? {
let image = UIImage(named: "qqq")!
print(text," ",image.size)
let imageView = UIImageView(image: image)
imageView.backgroundColor = UIColor.clear
imageView.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let label = UILabel(frame: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
label.font = UIFont.systemFont(ofSize: 50)
label.backgroundColor = UIColor.clear
label.textAlignment = .center
label.textColor = UIColor.black
label.text = text
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0)
imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
label.layer.render(in: UIGraphicsGetCurrentContext()!)
let imageWithText = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return imageWithText
}
#objc func textTapped(_ sender:UITapGestureRecognizer) {
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
return true
}
}
class SubTextAttachment:NSTextAttachment {
override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
let height = lineFrag.size.height
var scale: CGFloat = 1.0;
let imageSize = image!.size
if (height < imageSize.height) {
scale = height / imageSize.height
}
let value = CGRect(x: 0, y: descender, width: imageSize.width * scale, height: imageSize.height * scale)
return value
}
}
I know how to change the foreground color of any sub attributed string , but how i can know that the clicked part belong to the one to be colored ?
Also is there any better way to build this UI (in terms of performance ) as with tableView/CollectionView there is a dequeueing but here there isn't ?
So any hep is greatly appreciated
With NSAttributedString , you can use CoreText to render.
Convert NSAttributedString to CTFrame, then render it.
The key part
when you click a word in paragraph,
with override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
you can get a CGPoint
with that CGPoint & CTFrame, you can know the text range clicked in the text.
then rebuild the NSAttributedString 、CTFrame & rerender
here is the code you can refer
import UIKit
import CoreText
class TextRenderView: UIView {
let frameRef:CTFrame
let theSize: CGSize
let keyOne = //...
let keyTwo = //...
let rawTxt: String
let contentPage: NSAttributedString
let keyRanges: [Range<String.Index>]
override init(frame: CGRect){
rawTxt = //...
var tempRanges = [Range<String.Index>]()
if let rangeOne = rawTxt.range(of: keyOne){
tempRanges.append(rangeOne)
}
if let rangeTwo = rawTxt.range(of: keyTwo){
tempRanges.append(rangeTwo)
}
keyRanges = tempRanges
contentPage = NSAttributedString(string: rawTxt, attributes: [NSAttributedString.Key.font: UIFont.regular(ofSize: 15), NSAttributedString.Key.foregroundColor: UIColor.black])
let calculatedSize = contentPage.boundingRect(with: CGSize(width: UI.std.width - CGFloat(15 * 2), height: UI.std.height), options: [.usesFontLeading, .usesLineFragmentOrigin], context: nil).size
let padding: CGFloat = 10
theSize = CGSize(width: calculatedSize.width, height: calculatedSize.height + padding)
let framesetter = CTFramesetterCreateWithAttributedString(contentPage)
let path = CGPath(rect: CGRect(origin: CGPoint.zero, size: theSize), transform: nil)
frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
super.init(frame: frame)
backgroundColor = UIColor.white
}
required init?(coder: NSCoder) {
fatalError()
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else{
return
}
ctx.textMatrix = CGAffineTransform.identity
ctx.translateBy(x: 0, y: bounds.size.height)
ctx.scaleBy(x: 1.0, y: -1.0)
CTFrameDraw(frameRef, ctx)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else{
return
}
let pt = touch.location(in: self)
guard let offset = parserRect(with: pt, frame: frameRef), let pos = rawTxt.index(rawTxt.startIndex, offsetBy: offset, limitedBy: rawTxt.endIndex) else{
return
}
if keyRanges[0].contains(pos){
print(0)
}
else if keyRanges[1].contains(pos){
print(1)
}
}
func parserRect(with point: CGPoint, frame textFrame: CTFrame) -> Int?{
var result: Int? = nil
let path: CGPath = CTFrameGetPath(textFrame)
let bounds = path.boundingBox
guard let lines = CTFrameGetLines(textFrame) as? [CTLine] else{
return result
}
let lineCount = lines.count
guard lineCount > 0 else {
return result
}
var origins = [CGPoint](repeating: CGPoint.zero, count: lineCount)
CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), &origins)
for i in 0..<lineCount{
let baselineOrigin = origins[i]
let line = lines[i]
var ascent: CGFloat = 0
var descent: CGFloat = 0
var linegap: CGFloat = 0
let lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &linegap)
let lineFrame = CGRect(x: baselineOrigin.x, y: bounds.height-baselineOrigin.y-ascent, width: CGFloat(lineWidth), height: ascent+descent+linegap + 10)
if lineFrame.contains(point){
result = CTLineGetStringIndexForPosition(line, point)
break
}
}
return result
}
}
helper method:
extension String {
func range(ns inner: String) -> NSRange{
return (self as NSString).range(of: inner)
}
}
here is the github code you can refer

Swift - adding clear button to UITextfield programmatically

I am having problems adding a clear button to my UITextfield.
This is my textfield:
let emailTextField: CustomTextField = {
let v = CustomTextField()
v.borderActiveColor = .white
v.borderInactiveColor = .white
v.textColor = .white
v.font = UIFont(name: "AvenirNext-Regular", size: 17)
v.placeholder = "Email-Adresse"
v.placeholderColor = .gray
v.placeholderFontScale = 1
v.clearButtonMode = UITextField.ViewMode.always
v.minimumFontSize = 13
v.borderStyle = .line
v.autocapitalizationType = .none
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
As you can see I set clearButtonMode = .always but it is not being displayed.
My CustomTextFieldClass is nothing special either:
class CustomTextField: HoshiTextField {
/// the left padding
#IBInspectable public var leftPadding: CGFloat = 0 { didSet { self.setNeedsLayout() } }
/// the right padding
#IBInspectable public var rightPadding: CGFloat = 0 { didSet { self.setNeedsLayout() } }
/// Text rectangle
///
/// - Parameter bounds: the bounds
/// - Returns: the rectangle
override public func textRect(forBounds bounds: CGRect) -> CGRect {
let originalRect: CGRect = super.editingRect(forBounds: bounds)
return CGRect(x: originalRect.origin.x + leftPadding, y: originalRect.origin.y, width: originalRect.size.width - leftPadding - rightPadding, height: originalRect.size.height)
}
/// Editing rectangle
///
/// - Parameter bounds: the bounds
/// - Returns: the rectangle
override public func editingRect(forBounds bounds: CGRect) -> CGRect {
let originalRect: CGRect = super.editingRect(forBounds: bounds)
return CGRect(x: originalRect.origin.x + leftPadding, y: originalRect.origin.y, width: originalRect.size.width - leftPadding - rightPadding, height: originalRect.size.height)
}
Does anyone know why the clear button is not being displayed???

Change Width of UITextfield Input

Is there a way to change the input size of my UITextield so it doesn't overlap with my "vergessen"-Button?
The Textfield is a simple TextfieldView with a width of 315. I added the "Vergessen?" Button programmatically with the code down below.
func createForgetButton () {
let button = UIButton(type: .system)
button.setTitle("Vergessen?", for: .normal)
button.addTarget(self, action: #selector(self.vergessenTapped(_:)), for: .touchUpInside)
button.titleLabel?.font = UIFont(name: "Avenir Next", size: 19.0)
passwordTextField.rightView = button
passwordTextField.rightViewMode = .unlessEditing
}
Use the following custom class for the field. Then update .rightPadding field in IB or in the code to match the width of the button you have on the right.
/**
* Text field with some changes according to design
*
* - author: Alexander Volkov
* - version: 1.0
*/
#IBDesignable public class CustomTextField: UITextField {
/// the left padding
#IBInspectable public var leftPadding: CGFloat = 0 { didSet { self.setNeedsLayout() } }
/// the right padding
#IBInspectable public var rightPadding: CGFloat = 0 { didSet { self.setNeedsLayout() } }
/// Text rectangle
///
/// - Parameter bounds: the bounds
/// - Returns: the rectangle
override public func textRect(forBounds bounds: CGRect) -> CGRect {
let originalRect: CGRect = super.editingRect(forBounds: bounds)
return CGRect(x: originalRect.origin.x + leftPadding, y: originalRect.origin.y, width: originalRect.size.width - leftPadding - rightPadding, height: originalRect.size.height)
}
/// Editing rectangle
///
/// - Parameter bounds: the bounds
/// - Returns: the rectangle
override public func editingRect(forBounds bounds: CGRect) -> CGRect {
let originalRect: CGRect = super.editingRect(forBounds: bounds)
return CGRect(x: originalRect.origin.x + leftPadding, y: originalRect.origin.y, width: originalRect.size.width - leftPadding - rightPadding, height: originalRect.size.height)
}
}

Resize font along with frame of label using pinch gesture on UILabel?

Increase or decrease font size smoothly whenever user resize label using pinch gesture on it.
Note
Without compromising quality of font
Not only transforming the scale of UILabel
With support of multiline text
Rotation gesture should work proper with pinch gesture
Reference: SnapChat or Instagram Text Editor tool
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: UIFont(name: font.fontName, size: font.pointSize)!], context: nil)
return ceil(boundingBox.height)
}
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: UIFont(name: font.fontName, size: font.pointSize)!], context: nil)
return ceil(boundingBox.width)
}
}
func resizeLabelToText(textLabel : UILabel)
{
let labelFont = textLabel.font
let labelString = textLabel.text
let labelWidth : CGFloat = labelString!.width(withConstrainedHeight: textLabel.frame.size.height, font: labelFont!)
let labelHeight : CGFloat = labelString!.height(withConstrainedWidth: labelWidth, font: labelFont!)
textLabel.frame = CGRect(x: textLabel.frame.origin.x, y: textLabel.frame.origin.y, width: labelWidth, height: labelHeight)
textLabel.font = labelFont
}
func pinchedRecognize(_ pinchGesture: UIPinchGestureRecognizer) {
guard pinchGesture.view != nil else {return}
if (pinchGesture.view is UILabel) {
let selectedTextLabel = pinchGesture.view as! UILabel
if pinchGesture.state == .began || pinchGesture.state == .changed {
let pinchScale = round(pinchGesture.scale * 1000) / 1000.0
if (pinchScale < 1) {
selectedTextLabel.font = selectedTextLabel.font.withSize(selectedTextLabel.font.pointSize - pinchScale)
}
else {
selectedTextLabel.font = selectedTextLabel.font.withSize(selectedTextLabel.font.pointSize + pinchScale)
}
resizeLabelToText(textLabel: selectedTextLabel)
}
}
}
I solved the problem with following code which is working fine with every aspect which are mentioned in question, similar to Snapchat and Instagram:
var pointSize: CGFloat = 0
#objc func pinchRecoginze(_ pinchGesture: UIPinchGestureRecognizer) {
guard pinchGesture.view != nil else {return}
let view = pinchGesture.view!
if (pinchGesture.view is UILabel) {
let textLabel = view as! UILabel
if pinchGesture.state == .began {
let font = textLabel.font
pointSize = font!.pointSize
pinchGesture.scale = textLabel.font!.pointSize * 0.1
}
if 1 <= pinchGesture.scale && pinchGesture.scale <= 10 {
textLabel.font = UIFont(name: textLabel.font!.fontName, size: pinchGesture.scale * 10)
resizeLabelToText(textLabel: textLabel)
}
}
}
func resizeLabelToText(textLabel : UILabel) {
let labelSize = textLabel.intrinsicContentSize
textLabel.bounds.size = labelSize
}
Call following method every time after UILabel size changes.
func labelSizeHasBeenChangedAfterPinch(_ label:UILabel, currentSize:CGSize){
let MAX = 25
let MIN = 8
let RATE = -1
for proposedFontSize in stride(from: MAX, to: MIN, by: RATE){
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attribute = [NSAttributedString.Key.font:UIFont.systemFont(ofSize: CGFloat(proposedFontSize))]
// let context = IF NEEDED ...
let rect = NSString(string: label.text ?? "").boundingRect(with: currentSize, options: options, attributes: attribute, context: nil)
let labelSizeThatFitProposedFontSize = CGSize(width: rect.width , height: rect.height)
if (currentSize.height > labelSizeThatFitProposedFontSize.height) && (currentSize.width > labelSizeThatFitProposedFontSize.width){
DispatchQueue.main.async {
label.font = UIFont.systemFont(ofSize: CGFloat(proposedFontSize))
}
break
}
}
}
you can try:
1 - Set maximum font size for this label
2 - Set line break to Truncate Tail
3 - Set Autoshrink to Minimum font size (minimum size)

UITextfield gets cut from the top while in editing mode

I got two UITextfields with separator(UILabel) between them.
I put them all into UIStackView.
While in editing mode, content of the textfield is cut from the top, as seen in the picture below
I've found that the only way to remove this issue is to make this separator big enough, but this spoils my design.
How to fix it?
It's worth to mention my UIStackView settings:
and show how I implement this custom bottomline-style UITextfield
class CustomTextField: UITextField {
override func awakeFromNib() {
super.awakeFromNib()
let attributedString = NSAttributedString(string: self.placeholder!, attributes: [NSForegroundColorAttributeName:UIColor.lightGray, NSFontAttributeName: UIFont(name: "GothamRounded-Book", size: 18.0)! ])
self.attributedPlaceholder = attributedString
self.tintColor = UIColor.appRed
self.font = UIFont(name: "GothamRounded-Book", size: 18.0)!
self.borderStyle = .none
self.textAlignment = .center
}
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: 0, dy: 5)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: 0, dy: 5)
}
override var tintColor: UIColor! {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let startingPoint = CGPoint(x: rect.minX, y: rect.maxY)
let endingPoint = CGPoint(x: rect.maxX, y: rect.maxY)
let path = UIBezierPath()
path.move(to: startingPoint)
path.addLine(to: endingPoint)
path.lineWidth = 2.0
tintColor.setStroke()
tintColor = UIColor.appRed
path.stroke()
}
}
Any help much appreciated
EDIT
I have another TextField like that and it works fine, but it doesn't sit inside any horizontal UIStackView. Here is the screenshot of hierarchy:
Unfortunately you need to check the size on editing
class CustomTextField: UITextField {
override func awakeFromNib() {
super.awakeFromNib()
self.addTarget(self, action: #selector(textFieldEditingChanged), for: .editingChanged)
}
func textFieldEditingChanged(_ textField: UITextField) {
textField.invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
if isEditing {
let string = text ?? ""
let size = string.size(attributes: typingAttributes)
return CGSize(width: size.width + (rightView?.bounds.size.width ?? 0) + (leftView?.bounds.size.width ?? 0) + 2,
height: size.height)
}
return super.intrinsicContentSize
}
}

Resources