How to indent text in a label? Swift, UIKit - swift

I have a text in a label right aligned. The label is in a dynamic table cell. But the text is very close to the right edge of the label and does not look presentable. It looks something like this:
1234.5|
Update: How it looks on the screen. 0 is adjacent to the right side of the screen
I would like to move the text a little bit from the right edge to make it look like this:
1234.5--|
In the text field, I use a custom class for this and override the following functions:
private let padding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 20)
override func textRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
The label has an UIEdgeInsets object too. But I could not find functions similar to the functions of techRect and EditingRect on the label.
It seems to me that the label should have such functions. Maybe they are called differently. But I have not yet been able to find them in the documentation and Google search has not helped either.
Maybe someone has already solved such a problem and could suggest either the solution itself or in which direction to look. Any ideas are welcome. I really appreciate your help.

You could use a simple UILabel subclass with edge insets, like this:
class PaddedLabel: UILabel {
var padding: UIEdgeInsets = .zero
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: padding))
}
override var intrinsicContentSize : CGSize {
let sz = super.intrinsicContentSize
return CGSize(width: sz.width + padding.left + padding.right, height: sz.height + padding.top + padding.bottom)
}
}
Then set your custom "padding" like this:
label.padding = UIEdgeInsets(top: 0.0, left: 20.0, bottom: 0.0, right: 20.0)
Quick example:
class PaddedTestVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let defaultLabel = UILabel()
let paddedLabel = PaddedLabel()
[defaultLabel, paddedLabel].forEach { v in
v.backgroundColor = .green
v.text = "Test Text"
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
defaultLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
defaultLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
paddedLabel.topAnchor.constraint(equalTo: defaultLabel.bottomAnchor, constant: 40.0),
paddedLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
paddedLabel.padding = UIEdgeInsets(top: 12.0, left: 20.0, bottom: 12.0, right: 20.0)
}
}
The top label is a default UILabel the bottom label is a PaddedLabel - the only difference is the .padding:
A better option (in my view) would be to constrain your stack view to the margins of the cell's contentView - that way you get the defined margins (top/bottom and sides) spacing for the device and traits.

Related

Increasing hit area for UITapGestureRecognizer on UILabel

I'm trying to increase the hit area of a UITapGestureRecognizer on a UILabel object. This answer suggests overriding hitTest on the UILabel:
class PaddedLabel: UILabel {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print("hitTest called")
let padding: CGFloat = 20.0
let extendedBounds = bounds.insetBy(dx: -padding, dy: -padding)
return extendedBounds.contains(point) ? self : nil
}
}
However, the problem is that hitTest is not even called unless I'm actually tapping on the object, and not somewhere close to it. Therefore, extending the bounds seems to be useless.
The label is one of a few inside a UIStackView:
let label = PaddedLabel()
let gs = UITapGestureRecognizer(target: self, action: #selector(ThisViewController.handleTap(_:)))
label.addGestureRecognizer(gs)
label.isUserInteractionEnabled = true
stackView.addArrangedSubview(label)
How do I make this work?
You can edit PaddedLabel like below to set insets:
class PaddedLabel: UILabel {
var textInsets = UIEdgeInsets.zero {
didSet { invalidateIntrinsicContentSize() }
}
override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
let textRect = super.textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines)
let invertedInsets = UIEdgeInsets(top: -textInsets.top,
left: -textInsets.left,
bottom: -textInsets.bottom,
right: -textInsets.right)
return textRect.inset(by: invertedInsets)
}
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: textInsets))
}
}
and set textInsets to enlarge its tappable area.
label.textInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

Add left and right padding to UIButton

I would like to add some left and right padding to buttons, which I see they have a titleEdgeInsets property. Here's my code:
import UIKit
class QuickPromptButton: UIButton {
var userFacingValue: String?
var answerValue: String?
override init(frame: CGRect) {
super.init(frame: frame)
layer.borderColor = UIColor.primaryColor.cgColor
layer.borderWidth = 1
layer.cornerRadius = 15
setTitleColor(.primaryColor, for: .normal)
titleEdgeInsets = .init(top: 0, left: -10, bottom: 0, right: 10)
contentVerticalAlignment = .center
contentHorizontalAlignment = .center
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
However, this is what they look like now:
The problem is that the text goes out of the borders. Any idea why?
This is what I get by using 10 on both left and right:
override var intrinsicContentSize: CGSize {
let originalSize = super.intrinsicContentSize
let size = CGSize(width: originalSize.width + 20, height: originalSize.height)
return size
}
Overriding intrinsicContentSize and adding the total padding on top of the current width ended up working for me, although not sure if this is the right approach.

Table view automatic dimension forcing duplicate constraints

Getting automatic dimension working for UITableView.rowHeight requires a duplicate constraint in my UITableViewCell class.
I'm creating a UITableView programmatically (SwiftUI, no storyboard) and have cells of different heights. I’ve set the table’s rowHeight to UITableView.automaticDimension but can’t find the correct combination of constraints to get the table to actually calculate the correct height for the cells without adding a duplicate constraint.
I would expect to add a width, height, top, and leading constraint to get things working correctly. However, the table does not size the rows properly unless I also add a bottom constraint. Naturally this produces the warning:
2019-10-23 18:06:53.515025-0700 Test[15858:7764405] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x600001651c20 V:|-(0)-[UIView:0x7f8fc5a08890] (active, names: '|':Aries.TestTableViewCell:0x7f8fc5a084e0'TestTableViewCell' )>",
"<NSLayoutConstraint:0x600001651e50 UIView:0x7f8fc5a08890.bottom == Aries.TestTableViewCell:0x7f8fc5a084e0'TestTableViewCell'.bottom (active)>",
"<NSLayoutConstraint:0x600001651ea0 UIView:0x7f8fc5a08890.height == 40 (active)>",
"<NSLayoutConstraint:0x600001652120 'UIView-Encapsulated-Layout-Height' Aries.TestTableViewCell:0x7f8fc5a084e0'TestTableViewCell'.height == 40.3333 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x600001651ea0 UIView:0x7f8fc5a08890.height == 40 (active)>
If I remove the height constraint or the bottom anchor constraint the duplicate constraint is gone and the warning goes away. However, then the table won’t size the rows properly.
The View:
import SwiftUI
struct TableViewTest: View {
var body: some View {
TestTableView().frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
}
}
The TableView:
import SwiftUI
struct TestTableView: UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: UIViewRepresentableContext<TestTableView>) -> UITableView {
let tableView = UITableView(frame: .zero)
tableView.register(TestTableViewCell.self, forCellReuseIdentifier: "TestTableViewCell")
tableView.rowHeight = UITableView.automaticDimension
let dataSource = UITableViewDiffableDataSource<Section, TestData>(tableView: tableView) { tableView, indexPath, data in
let cell = tableView.dequeueReusableCell(withIdentifier: "TestTableViewCell", for: indexPath) as! TestTableViewCell
cell.data = data
return cell
}
populate(dataSource: dataSource)
context.coordinator.dataSource = dataSource
return tableView
}
func populate(dataSource: UITableViewDiffableDataSource<Section, TestData>) {
let items = [
TestData(color: .red, size: CGSize(width: 40, height: 20)),
TestData(color: .blue, size: CGSize(width: 40, height: 40)),
TestData(color: .green, size: CGSize(width: 40, height: 80))
]
var snapshot = NSDiffableDataSourceSnapshot<Section, TestData>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot)
}
func updateUIView(_ tableView: UITableView, context: UIViewRepresentableContext<TestTableView>) {
guard let dataSource = context.coordinator.dataSource else {
return
}
populate(dataSource: dataSource)
}
class Coordinator {
var dataSource: UITableViewDiffableDataSource<Section, TestData>?
}
enum Section {
case main
}
struct TestData: Hashable {
var id = UUID()
var color: UIColor
var size: CGSize
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
}
The Table View Cell:
import UIKit
class TestTableViewCell: UITableViewCell {
var data: TestTableView.TestData? {
didSet {
if let data = data {
let view = UIView()
view.backgroundColor = data.color
addSubview(view)
// !!! SETTING THE BOTTOM ANCHOR TO NIL OR HEIGHT TO 0 PREVENTS THE TABLE FROM SIZING THE ROWS CORRECTLY
view.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: data.size.width, height: data.size.height, enableInsets: false)
}
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The UIView Anchor Extension:
func anchor (top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddingLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat, enableInsets: Bool) {
var topInset = CGFloat(0)
var bottomInset = CGFloat(0)
if #available(iOS 11, *), enableInsets {
let insets = self.safeAreaInsets
topInset = insets.top
bottomInset = insets.bottom
}
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
self.topAnchor.constraint(equalTo: top, constant: paddingTop+topInset).isActive = true
}
if let left = left {
self.leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
}
if let right = right {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom-bottomInset).isActive = true
}
if height != 0 {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
if width != 0 {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
}
If I anchor only the top left corner and specify a width and height in the table view cell, the table view will not calculate the heights correctly. However, if I also specify the height, the rows will be sized correctly but a warning is generated about a duplicate constraint. Is there a magic combination that allows correct layout but does not produce the warning?
Bad (but no duplicate constraints):
Good (with duplicate constraints):
To fix this issue, you need to change the priority of your bottom constraint and then the warning will go away.
if let bottom = bottom {
let bottomConstraint = bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom-bottomInset)
bottomConstraint.priority = UILayoutPriority(750)
bottomConstraint.isActive = true
}

Is possible make a UITextField like this?

Situation: I want to use a custom UITextField class for my textField in a xcode project.
I want to the textField look like this:
I had no problems in making the edges rounded, and change the color of my placeholder, but I have no idea how to keep the bottom edges flat and draw a black border only on the bottom.
This is my code:
import UIKit
class GrayTextField: UITextField {
override func awakeFromNib() {
super.awakeFromNib()
backgroundColor = .grayf1f1f1
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
layer.cornerRadius = 10
clipsToBounds = true
}
override var placeholder: String? {
didSet {
let attributes = [ NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16, weight: .thin)]
attributedPlaceholder = NSAttributedString(string: placeholder ?? "", attributes: attributes)
}
}
}
And my current result:
In your answer, still there is some issue in bottom left and right corner.
To achieve exact result, change your UITextField Border Style to No Border.
Padding for Text:
class GrayTextField: UITextField {
let padding = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 5)
..... Your Exact Code .....
override open func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: padding)
}
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: padding)
}
override open func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: padding)
}
}
OutPut
Well, after some researching, i do it.
Here if my final code:
import UIKit
class GrayTextField: UITextField {
override func awakeFromNib() {
super.awakeFromNib()
backgroundColor = .grayf1f1f1
clipsToBounds = true
let maskPath = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: 10.0, height: 10.0))
let shape = CAShapeLayer()
shape.path = maskPath.cgPath
layer.mask = shape
addBottomBorder(with: .darkGray, andWidth: 1)
}
override var placeholder: String? {
didSet {
let attributes = [ NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16, weight: .thin)]
attributedPlaceholder = NSAttributedString(string: placeholder ?? "", attributes: attributes)
}
}
func addBottomBorder(with color: UIColor?, andWidth borderWidth: CGFloat) {
let border = UIView()
border.backgroundColor = color
border.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
border.frame = CGRect(x: 0, y: frame.size.height - borderWidth, width: frame.size.width, height: borderWidth)
addSubview(border)
}
}
And this is the result:

UITextView Horizontal & Vertical Scroll With Proper Cursor Positioning in Swift

I am working on a project where I require to make UITextView horizontally scrollable.The problem statement is defined below
I have tried putting the UITextview within a ScrollView for horizontal scroll as suggested in other similar question solution on Stack Overflow, while the scroll works there are multiple issues related to cursor position like:
Dynamically setting the width of UITextView to match the width of content's biggest line (achievable)
Cursor doesn't show on the screen when content increase or decrease i.e while adding content in same line or deleting content in between, cursor position is not handled by UITextView perfectly the cursor jumps to line start and comes back to current position, and is not visible on screen.
UITextView should only be able to scroll Horizontally or Vertically.(Need to disable the diagonal scroll).
Attaching the current code I am experimenting with, also tried other answers in similar questions, doesn't work.:
class ViewController: UIViewController, UITextViewDelegate, UIScrollViewDelegate {
#IBOutlet weak var scroll:UIScrollView!
#IBOutlet weak var textView:UITextView!
var displayStr = ""
var strSize:CGRect!
var font:UIFont!
var maxSize:CGSize!
override func viewDidLoad() {
super.viewDidLoad()
displayStr = textView.text
textView.delegate = self
scroll.delegate = self
scroll.addSubview(textView)
textView.isEditable = true
textView.isScrollEnabled = false
maxSize = CGSize(width: 9999, height: 9999)
font = UIFont(name: "Menlo", size: 16)!
textView.font = font
updateWidth()
}
func updateWidth() {
strSize = (displayStr as NSString).boundingRect(with: maxSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font : font!], context: nil)
if strSize.width > self.view.frame.width {
textView.frame = CGRect(x: 0, y: 0, width: strSize.width + 50, height: view.frame.height+10)
}
scroll.frame = CGRect(x: 0, y: 100, width: view.frame.width, height: view.frame.height)
scroll.contentSize = CGSize(width: strSize.width + 30, height: strSize.height)
}
func textViewDidChange(_ textView: UITextView) {
updateWidth()
}
This is how my output looks