Multiline UIButton with each line truncated independently - ios

I'm trying to make a multiline button by subclassing UIButton. To avoid drawing two custom UILabel (I'm still pretty new to Swift/Xcode), I'm using attributed strings for the existing UILabel and splitting lines with a new line character, like so:
func prepareAttributedTitle(_ primaryTitle: String = "", _ secondaryTitle: String = "") {
let title = NSMutableAttributedString()
let first = NSAttributedString(string: primaryTitle, attributes: [
NSForegroundColorAttributeName: tintColor,
NSFontAttributeName: UIFont.systemFont(ofSize: UIFont.systemFontSize, weight: UIFontWeightSemibold)
])
let newLine = NSAttributedString(string: "\n")
let second = NSAttributedString(string: secondaryTitle, attributes: [
NSForegroundColorAttributeName: tintColor.withAlphaComponent(0.75),
NSFontAttributeName: UIFont.systemFont(ofSize: UIFont.smallSystemFontSize)
])
title.append(first)
title.append(newLine)
title.append(second)
setAttributedTitle(title, for: .normal)
}
And the result is (sorry, I don't have enough rep to post images):
| This is the long first |
| line |
| Secondary line |
However, I'd like to truncate lines independently, like this:
| This is the long fi... |
| Secondary line |
Is there a way to do this without using two custom UILabels?
Thanks

A single UILabel does not support what you need. You will have to use two single-line labels each set with tail truncation.

I'm answering my own question with what worked for me. Here's my subclass of UIButton, but keep in mind I'm not an experienced developer. There's also some styling and support for the tint color:
import UIKit
#IBDesignable
class FilterButton: UIButton {
let primaryLabel = UILabel()
let secondaryLabel = UILabel()
#IBInspectable var primaryTitle = "" {
didSet {
primaryLabel.text = primaryTitle
}
}
#IBInspectable var secondaryTitle = "" {
didSet {
secondaryLabel.text = secondaryTitle
}
}
// MARK: Initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
override func prepareForInterfaceBuilder() {
primaryTitle = "Primary title"
secondaryTitle = "Secondary title"
commonInit()
}
func commonInit() {
// Force left alignment (FIXME: Use user language direction)
contentHorizontalAlignment = .left
// Set some padding and styling
contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
layer.cornerRadius = 5
layer.borderWidth = 1
// Hide button original label
titleLabel?.isHidden = true
// Prepare the primary label
primaryLabel.frame = CGRect(x: contentEdgeInsets.left,
y: contentEdgeInsets.top,
width: frame.width - contentEdgeInsets.left - contentEdgeInsets.right,
height: (frame.height - contentEdgeInsets.top - contentEdgeInsets.bottom) / 2)
primaryLabel.font = UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)
primaryLabel.textColor = tintColor
// primaryLabel.backgroundColor = UIColor.green // For debugging
primaryLabel.lineBreakMode = .byTruncatingMiddle // Truncate first line
primaryLabel.autoresizingMask = .flexibleWidth
addSubview(primaryLabel)
// Prepare the secondary label
secondaryLabel.frame = CGRect(x: contentEdgeInsets.left,
y: contentEdgeInsets.top + primaryLabel.frame.height,
width: frame.width - contentEdgeInsets.left - contentEdgeInsets.right,
height: (frame.height - contentEdgeInsets.top - contentEdgeInsets.bottom) / 2)
secondaryLabel.font = UIFont.systemFont(ofSize: UIFont.smallSystemFontSize)
secondaryLabel.textColor = tintColor.withAlphaComponent(0.75)
// secondaryLabel.backgroundColor = UIColor.yellow // For debugging
secondaryLabel.lineBreakMode = .byTruncatingMiddle // Truncate second line
secondaryLabel.autoresizingMask = .flexibleWidth
addSubview(secondaryLabel)
primaryLabel.text = primaryTitle
secondaryLabel.text = secondaryTitle
}
// Support tint color
override func tintColorDidChange() {
super.tintColorDidChange()
layer.borderColor = tintColor.cgColor
layer.backgroundColor = tintColor.withAlphaComponent(0.05).cgColor
primaryLabel.textColor = tintColor
secondaryLabel.textColor = tintColor.withAlphaComponent(0.75)
}
}

Related

Search Bar textfield cursor color not changing in mac Catalyst it's showing black color ios swift

i also try to change with "searchController.searchBar.searchTextField.tintColor = .white"
but it's not working issue facing after xcode 13 update.
Try to create a custom searchController and into the setup to change the tintColor of all the subviews that are different of UIButton .
Here an example :
class CustomSearchController: UISearchController {
var placeHolder:String?
private var catalogSearchBar = CatalogSearchBar()
override public var searchBar: UISearchBar {
get {
catalogSearchBar.placeholder = placeHolder
return catalogSearchBar
}
}
}
class CatalogSearchBar: UISearchBar {
init() {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
setup()
}
private func setup() {
backgroundColor = Constants.shared.navigationBar.lightModeBgColor
// text field
let textField = searchTextField
textField.subviews.forEach { (view) in
if ((view as? UIButton) != nil) {
view.tintColor = UIColor.white
}
}
textField.frame.size.height = 35
self.searchTextPositionAdjustment = UIOffset(horizontal: 4, vertical: 0)
textField.layer.cornerRadius = 15
textField.placeholder = self.placeholder
textField.attributedPlaceholder = NSAttributedString(string: self.placeholder != nil ? self.placeholder! : "", attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
textField.layer.masksToBounds = true
textField.layer.backgroundColor = UIColor.white.withAlphaComponent(0.25).cgColor
if let view = textField.value(forKey: "backgroundView") as? UIView {
view.removeFromSuperview()
}
textField.font = UIFont(name: "Montserrat-Regular", size: 15)
textField.textColor = UIColor.white
textField.tintColor = UIColor.white
// search icon
let leftView: UIView = {
let image = UIImage(named: "search")
let padding = 8
let size = 20
let outerView = UIView(frame: CGRect(x: 0, y: 0, width: size + padding, height: size) )
let iconView = UIImageView(frame: CGRect(x: padding, y: 0, width: size, height: size))
iconView.tintColor = UIColor.white
iconView.image = image
outerView.addSubview(iconView)
return outerView
}()
textField.leftView = leftView
}
}

How to access text views in custom collection view cell class from the main class

I've got two text views in each of my collection view cells, I've made a custom class for the cells. here is the code:
class CustomWriterPageCell: UICollectionViewCell {
fileprivate let textViewOne: UITextView = {
let tv = UITextView()
tv.backgroundColor = .cyan
tv.text = "Chapter Title"
tv.font = UIFont(name: "Avenir-Roman", size: 27)
tv.textColor = .gray
return tv
}()
fileprivate let textViewTwo: UITextView = {
let tv = UITextView()
tv.textColor = .gray
tv.text = "Start your story..."
tv.textContainerInset = UIEdgeInsets(top: 20, left: 15, bottom: 20, right: 15)
tv.font = UIFont(name: "Avenir-Book", size: 23)
tv.backgroundColor = .black
return tv
}()
}
I would like to add placeholders to both these text views but the problem is that since they are in a custom class there is no way, that I know of, to find out which text view is being edited so I can't add the respective placeholders back when textViewDidEndEditing takes place, is there a way to find out which text view is being edited? Is there a way to access the text views from the main class?
In your CustomWriterPageCell class:
fileprivate let textViewOne: UITextView = {
let tv = UITextView()
tv.backgroundColor = .cyan
tv.text = "Chapter Title"
tv.font = UIFont(name: "Avenir-Roman", size: 27)
tv.textColor = .gray
return tv
}()
fileprivate let textViewTwo: UITextView = {
let tv = UITextView()
tv.textColor = .gray
tv.text = "Start your story..."
tv.textContainerInset = UIEdgeInsets(top: 20, left: 15, bottom: 20, right: 15)
tv.font = UIFont(name: "Avenir-Book", size: 23)
tv.backgroundColor = .black
return tv
}()
override init(frame: CGRect) {
super.init(frame: frame)
textViewOne.delegate = self
textViewTwo.delegate = self
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then:
extension CustomWriterPageCell : UITextViewDelegate {
func textViewDidEndEditing(_ textView: UITextView) {
if textView == textViewOne {
textView.text = "..."
}
else if textView == textViewTwo {
textView.text = "..."
}
}
}
This way, your View Controller still does not need to know about your text views.
You can always access the actual textView and know which one is being edited in delegate methods, just like this:
class ViewController: UIViewController {
var textViewA: UITextView = .init()
var textViewB: UITextView = .init()
override func viewDidLoad() {
super.viewDidLoad()
textViewA.delegate = self
textViewB.delegate = self
}
}
extension ViewController: UITextViewDelegate {
func textViewDidEndEditing(_ textView: UITextView) {
switch textView {
case textViewA:
textView.text = "Placeholder A"
case textViewB:
textView.text = "Placeholder B"
default:
break
}
}
}
Also, because of the fact that UITextView doesn't have any built-in placeholder mechanism - you could use this nice pod: https://github.com/devxoul/UITextView-Placeholder
If you use this pod and don't want to make textViews public/internal - you can create computedProperties for it:
var placeholder: String {
get {
return textView.placeholder
} set {
textView.placeholder = newValue
}
}

Swift: Make vertical scrolling feed with programmatically sized UIViews

I'm building a scrolling feed in my app with data grabbed from a database (firebase). I'm not very experienced in Swift as most of the stuff I do is web design. What I'm looking for is a good way to size the height of my UIViews. Here is what I currently have (fixed height):
Here's my code UIView class:
class eventView: UIView {
let eventDate : UILabel = {
let eventDate = UILabel()
eventDate.translatesAutoresizingMaskIntoConstraints = false
eventDate.numberOfLines = 0
eventDate.textAlignment = .center
return eventDate
}()
let eventTitle : UILabel = {
Same thing as eventDate
}()
let eventDesc : UILabel = {
same thing as eventDate
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.lightGray
self.layer.cornerRadius = 10
self.addSubview(eventDate)
eventDate.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
eventDate.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
self.addSubview(eventTitle)
eventTitle.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
eventTitle.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
eventTitle.topAnchor.constraint(equalTo: eventDate.bottomAnchor).isActive = true
self.addSubview(eventDesc)
eventDesc.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
eventDesc.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
eventDesc.topAnchor.constraint(equalTo: eventTitle.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is my for list that displays these UIViews:
var i = 0
for data in self.eventViewData {
let view = eventView(frame: CGRect(x: 0, y: ( CGFloat(170 * i)), width: self.scrollView.frame.width - 20, height: CGFloat(150)))
view.center.x = self.scrollView.center.x
view.eventDate.text = data.date
view.eventTitle.text = data.title
view.eventDesc.text = data.description
self.scrollView.addSubview(view)
i += 1
}
I usually am using HTML div's and such so I'm having a hard time figuring out how to style these. Any information or links to tutorials on how to programmatically adjust constraints to the UILabels in my eventViews are also appreciated.
How to calculate the desired height seems to be the question. While there are a lot of ways to do this one approach would be something like this:
func heightForView(text:String, font:UIFont, width:CGFloat) -> CGFloat{
let label:UILabel = UILabel(frame: CGRectMake(0, 0, width, CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = text
label.sizeToFit()
return label.frame.height
}
let font = UIFont(name: "Helvetica", size: 20.0)
var height = heightForView("This is just a load of text", font: font, width: 100.0)
// apply height to desired view
I prefer to put the heightForView function in an extension myself but it isn't required.
I assumed you're using table view, why not do dynamic height? Good reference https://www.raywenderlich.com/1067-self-sizing-table-view-cells
In essense, you use auto layout to define your top and bottom constraint accordingly for every element in your cell, and your table view delegation/data source method for heightforrow and estimatedheightforrow, return UITableViewAutomaticDimension

Custom UITextField with UILabel multiline support for error text

I want to create custom UITextField with error label on bottom of it. I want the label to be multiline, I tried numberOfLines = 0. But it is not working.
Here is my snippet for the class
public class MyTextField: UITextField {
private let helperTextLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12.0, weight: UIFont.Weight.regular)
label.textColor = helperTextColor
label.numberOfLines = 0
return label
}()
public override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(errorTextLabel)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addSubview(errorTextLabel)
}
public override func layoutSubviews() {
super.layoutSubviews()
errorTextLabel.frame = CGRect(x: bounds.minX, y: bounds.maxY - 20, width: bounds.width, height: 20)
}
public override var intrinsicContentSize: CGSize {
return CGSize(width: 240.0, height: 68.0)
}
public override func sizeThatFits(_ size: CGSize) -> CGSize {
return intrinsicContentSize
}
}
I think the root cause is because I set height to 20, but how can I set the height dynamically based on the errorTextLabel.text value?
You are giving your label a fixed size.
Not using AutoLayout and giving the textfield and label room to expand it's size when needed.
Personally, if creating this particular control, I would create a UIView with a textfield and a label inside a UIStackView. That way if the label is hidden when there is no error the stackview will automatically adjust the height for you. Then when you unhide it, the view will expand to fit both controls.
A basic example:
//: Playground - noun: a place where people can play
import UIKit
import PlaygroundSupport
class LabelledTextView: UIView {
private let label = UILabel()
private let textfield = UITextField()
private let stackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true
stackView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
stackView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
stackView.alignment = .leading
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.addArrangedSubview(textfield)
stackView.addArrangedSubview(label)
textfield.placeholder = "Please enter some text"
label.numberOfLines = 0
label.text = "Text did not pass validation, Text did not pass validation, Text did not pass validation, Text did not pass validation"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let labelledTextView = LabelledTextView(frame: CGRect(x: 50, y: 300, width: 300, height: 60))
let vc = UIViewController()
vc.view.addSubview(labelledTextView)
labelledTextView.translatesAutoresizingMaskIntoConstraints = false
labelledTextView.topAnchor.constraint(equalTo: vc.view.topAnchor).isActive = true
labelledTextView.leftAnchor.constraint(equalTo: vc.view.leftAnchor).isActive = true
labelledTextView.widthAnchor.constraint(equalToConstant: 300).isActive = true
labelledTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
PlaygroundPage.current.liveView = vc.view
You need to use sizeToFit():
errorTextLabel.sizeToFit()

Swift UILabel subclass automatically call enum switch function

I have been creating a UILabel class called RPLabel, which is supposed to shorten all of my long list of programmatically set labels. This is the class code:
class RPLabel: UILabel {
// moneyTitle.frame = CGRect(x: 50, y: -50, width:XX, height: 35) //
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.setup()
self.testForLabelType()
}
enum labelTypeEnumeration {
case title, subtitle
}
var labelType = labelTypeEnumeration.title
override init(frame: CGRect){
super.init(frame: frame)
self.setup()
}
override func awakeFromNib() {
super.awakeFromNib()
self.setup()
}
func setup(){
self.text = self.text
self.textColor = self.textColor
self.font = self.font
self.layer.display()
}
func testForLabelType() {
switch labelType {
case .title:
setupTitle()
print("setupTitle")
case .subtitle:
setupSubtitle()
print("setupSubtitle")
}
}
func setupTitle(){
self.font = UIFont.boldSystemFont(ofSize: 12)
self.textColor = UIColor.secondaryColor()
self.textAlignment = .left
}
func setupSubtitle(){
self.font = UIFont.boldSystemFont(ofSize: 18)
self.textColor = UIColor.rgb(red: 200, green: 200, blue: 200)
self.textAlignment = .left
}
}
To make a label, I use the following piece of code:
var moneyTitle = RPLabel()
moneyTitle.labelType = .title
moneyTitle.testForLabelType()
moneyTitle.text = "MONEY"
moneyTitle.frame = CGRect(x: 50, y: -50, width:XX, height: 35)
The issue that I have is that I would like to set the labelType to .title without having to write moneyTitle.testForLabelType(). In other words, I would like for that function to run automatically in the class. An alternative I would not like would be inserting the parameter in the init, so please avoid telling me to do so, thanks.
You can use didSet:
didSet is called immediately after the new value is stored.
var labelType = labelTypeEnumeration.title {
didSet {
testForLabelType()
}
}

Resources