Custom UITableViewCell labels - ios

I'm trying to create a custom UITableViewCell with 3 labels, in a similar style to the subtitle style, but with a label on the right, too. I've got 2 issues, so far:
When creating the labels on the left, I was using a constant 16 offset, but have since found this is incorrect. The separator inset returns 15 (self.separatorInset.left), but iPad the labels seem to be 22.5 from the left. I'll have to do this in code, but is checking for the iPad UI idiom and multiplying by 1.5 the correct way of doing this?
The second issue is that I am using dynamic font sizes: Body for the main label and Caption 1 for the subtitle. However, this creates labels with font size 17 and 12 respectively, while the system cell's labels are 16 and 11. I've got a custom UILabel that tries to ensure system font size changes are respected. Should I make the font size 1 smaller in this class?
class DynamicFontLabel: UILabel {
var fontStyle: String?
convenience override init() {
self.init(fontStyle: UIFontTextStyleBody)
}
init(fontStyle: String) {
super.init()
self.font = UIFont.preferredFontForTextStyle(fontStyle)
self.setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
func setup() {
// Ensure the text color is correct, as per the UILabel appearance
self.textColor = UILabel.appearance().textColor
self.fontStyle = self.currentFontStyle()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateFontSize", name: UIContentSizeCategoryDidChangeNotification, object: nil)
self.numberOfLines = 0
self.lineBreakMode = NSLineBreakMode.ByWordWrapping
}
func currentFontStyle() -> String {
if self.font.isEqual(UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)) {
return UIFontTextStyleHeadline
} else if self.font.isEqual(UIFont.preferredFontForTextStyle(UIFontTextStyleBody)) {
return UIFontTextStyleBody
} else if self.font.isEqual(UIFont.preferredFontForTextStyle(UIFontTextStyleCaption1)) {
return UIFontTextStyleCaption1
} else if self.font.isEqual(UIFont.preferredFontForTextStyle(UIFontTextStyleCaption2)) {
return UIFontTextStyleCaption2
} else if self.font.isEqual(UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)) {
return UIFontTextStyleFootnote
} else if self.font.isEqual(UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)) {
return UIFontTextStyleSubheadline
}
return UIFontTextStyleBody
}
func updateFontSize() {
if let fontStyle = self.fontStyle {
self.font = UIFont.preferredFontForTextStyle(fontStyle)
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}

Related

Set different font as per title and subtitle in UIlabel IBDesignable

I had created the custom class for uilabel with below code:
#IBDesignable
public class CustomUILabel: UILabel {
public override func awakeFromNib() {
super.awakeFromNib()
configureLabel()
}
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
configureLabel()
}
func configureLabel() {
textColor = .white
font = UIFont(name: Constants.regularFontName, size: 14)
}
}
This help me to set the font through out the application.But i wanted to create different bold font type for title and regular font for subtitle.
Is this possible only with one file ?
Or i need to create different Extension for that type of UIlabel
You could for example add a custom style property like this:
#IBDesignable
public class CustomUILabel: UILabel {
enum CustomUILabelStyle {
case title, subtitle
var font: UIFont? {
switch self {
case .title:
return UIFont(name: Constants.boldFontName, size: 14)
case .subtitle:
return UIFont(name: Constants.regularFontName, size: 14)
}
}
var textColor: UIColor {
switch self {
// add cases if you want different colors for different styles
default: return .white
}
}
}
var style: CustomUILabelStyle = .title {
didSet {
// update the label's properties after changing the style
if style != oldValue {
configureLabel()
}
}
}
public override func awakeFromNib() {
super.awakeFromNib()
configureLabel()
}
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
configureLabel()
}
func configureLabel() {
font = style.font
textColor = style.textColor
}
}
You use it like this:
let label = CustomUILabel()
label.style = .title
// label.style = .subtitle

AutoLayout: multiline label and fixed-size button

I'm trying to replicate the following layout from the Kayak app:
The layout consists of UICollectionViewCell with a UILabel and a INUIAddVoiceShortcutButton.
However, in my implementation the label doesn't force the cell to stretch further when the text doesn't fit:
How could I make the UICollectionViewCell grow with the label, and not truncate the label to the size of the cell?
The whole code for the cell:
final class AddToSiriCell: CornerMaskCellBase {
lazy var button: INUIAddVoiceShortcutButton = {
let b = INUIAddVoiceShortcutButton(style: .whiteOutline)
return b
}()
lazy var textLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
configureViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configureViews() {
textLabel.text = "View balance with your pre-recorded Siri Command .View balance with your pre-recorded Siri Command View balance with your pre-recorded Siri Command View balance with your pre-recorded Siri Command "
contentView.backgroundColor = .white
[button, textLabel].forEach(contentView.addSubview)
button.snp.makeConstraints { (make) in
make.top.bottom.trailing.equalTo(contentView.layoutMarginsGuide)
}
textLabel.snp.makeConstraints { (make) in
make.top.bottom.leading.equalTo(contentView.layoutMarginsGuide).priority(.required)
make.trailing.equalTo(button.snp.leading).priority(.required)
}
}
}
Update 1: Added "Base Class" with fixed width
Here is the base class I use for all the cells in the UICollectionView:
import UIKit
import SnapKit
class AutoSizingCellBase: UICollectionViewCell {
override class var requiresConstraintBasedLayout: Bool {
return true
}
private final var widthConstraint: Constraint?
override init(frame: CGRect) {
super.init(frame: frame)
contentView.layoutMargins = UIEdgeInsets(padding: 14)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
if widthConstraint == nil {
if let window = window {
let width = window.bounds.width - 16
contentView.snp.makeConstraints { (make) in
widthConstraint = make.width.equalTo(width).priority(.required).constraint
}
}
contentView.translatesAutoresizingMaskIntoConstraints = true
}
super.updateConstraints()
}
}
Set your top constraint on both the label and the button to greaterThanOrEqual
Set your bottom constraint on both the label and the button to lessThanOrEqual
Edit:
Both should also have centerY constraints.
Here is a complete example (I'm not on iOS 12, so I used a standard UIButton in place of INUIAddVoiceShortcutButton). I also set the background of the label to cyan to make it easy to see its resulting frame:
//
// SnapTableViewController.swift
//
// Created by Don Mag on 10/19/18.
//
import UIKit
class SnapCell: UITableViewCell {
lazy var theButton: UIButton = {
let b = UIButton()
b.backgroundColor = .yellow
b.setTitle("Add to Siri", for: .normal)
b.setTitleColor(.black, for: .normal)
b.layer.cornerRadius = 8
b.layer.borderColor = UIColor.black.cgColor
b.layer.borderWidth = 1
return b
}()
lazy var theLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.backgroundColor = .cyan
return label
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configureViews()
}
func configureViews() -> Void {
contentView.backgroundColor = .white
[theButton, theLabel].forEach(contentView.addSubview)
// constrain button size to 120 x 40
theButton.snp.makeConstraints { (make) in
make.width.equalTo(120)
make.height.equalTo(40)
}
// constrain button to trailing margin
theButton.snp.makeConstraints { (make) in
make.trailing.equalTo(contentView.layoutMarginsGuide)
}
// constrain button top to greaterThanOrEqualTo margin
theButton.snp.makeConstraints { (make) in
make.top.greaterThanOrEqualTo(contentView.layoutMarginsGuide)
}
// constrain button bottom to lessThanOrEqualTo margin
theButton.snp.makeConstraints { (make) in
make.bottom.lessThanOrEqualTo(contentView.layoutMarginsGuide)
}
// also constrain button to centerY
theButton.snp.makeConstraints { (make) in
make.centerY.equalTo(contentView.snp.centerY)
}
// constrain label to leading margin
theLabel.snp.makeConstraints { (make) in
make.leading.equalTo(contentView.layoutMarginsGuide)
}
// constrain label top to greaterThanOrEqualTo margin
theLabel.snp.makeConstraints { (make) in
make.top.greaterThanOrEqualTo(contentView.layoutMarginsGuide)
}
// constrain label bottom to lessThanOrEqualTo margin
theLabel.snp.makeConstraints { (make) in
make.bottom.lessThanOrEqualTo(contentView.layoutMarginsGuide)
}
// also constrain label to centerY
theLabel.snp.makeConstraints { (make) in
make.centerY.equalTo(contentView.snp.centerY)
}
// constrain label trailing to 8-pts from button leading
theLabel.snp.makeConstraints { (make) in
make.trailing.equalTo(theButton.snp.leading).offset(-8)
}
}
}
class SnapTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SnapCell", for: indexPath) as! SnapCell
switch indexPath.row % 4 {
case 0:
cell.theLabel.text = "One line label."
case 1:
cell.theLabel.text = "This label has\nTwo Lines."
case 2:
cell.theLabel.text = "This label has enough text that is will wrap to Three Lines (on an iPhone 7)."
default:
cell.theLabel.text = "View balance with your pre-recorded Siri Command .View balance with your pre-recorded Siri Command View balance with your pre-recorded Siri Command View balance with your pre-recorded Siri Command "
}
return cell
}
}
Set the label's top, bottom and left constraints with superview i.e cell's content view. Now give your button to fixed height and width constraints and provide top, left and right margins, left margin should be with your label. Now set your label's number of lines property as zero. Any doubt please comment.

How to make link, phone number clickable (Same behaviour as in textview) in a custom view where i draw text?

I made a view and draw text on it and i want that if any text contains link(Hyperlink) or Phone Number It would be clickable (Same Behaviour As in Text View) So how to Achieve it ?
Code For View In which i am Drawing Text :-
class DrawRectCellView: UIView {
var text: NSAttributedString?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Only override drawRect: if you perform custom drawing.
override func draw(_ rect: CGRect)
{
UIColor.white.setFill()
UIGraphicsGetCurrentContext()?.fill(rect)
// Drawing code
if let attributedText = text {
attributedText.draw(in: rect)
}
}
}
Code For TableCell :-
class DrawRectCell: UITableViewCell {
var cellView: DrawRectCellView?
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// Initialization code
cellView = DrawRectCellView(frame: self.frame)
if let cell = cellView {
cell.autoresizingMask = UIViewAutoresizing(rawValue: UIViewAutoresizing.RawValue(UInt8(UIViewAutoresizing.flexibleWidth.rawValue) | UInt8(UIViewAutoresizing.flexibleHeight.rawValue)))
cell.contentMode = UIViewContentMode.redraw
}
self.contentView.addSubview(cellView!)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setTextString(_ text: NSAttributedString) {
if let view = cellView {
view.text = text
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
i am Setting Text like = www.google.com or any phone number its showing as normal text only (Not Showing Like In textview (it makes it clickable))
First you need to detect your text contain url or numbers like this way.
let input = "This is a test with the URL https://www.hackingwithswift.com to be detected."
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
for match in matches {
guard let range = Range(match.range, in: input) else { continue }
let url = input[range]
print(url)
}
if you detect url after setting to the textview you need to add UITapGestureRecognizer on UITextView like this way.
let tapGesture = UITapGestureRecognizer(target: self, action: "handleTap")
textview.addGestureRecognizer(tapGesture)
func handleTap(sender: UITapGestureRecognizer) {
if sender.state == .began {
//write your code here for url tap
}
}
You can add UITapGestureRecognizer on DrawRectCellView and set the IndexPath as tag. In the selector method of UITapGestureRecognizer you can get the subviews info of the tapped contentView containing text.
To highlight certain text within the TextView:
Let's say you have your textView textView, then you can use this code to highlight URLs, phone numbers, etc.:
textView.dataDetectorTypes = [] // doesn't work if textfield isEditable is set to true
textView.linkTextAttributes = [NSForegroundColorAttributeName: UIColor.blue] // the default is blue anyway
If you want to only include some specific data types to be detected, add them to the array like so:
textView.dataDetectorTypes = [.link]
Here's a list of types:
https://developer.apple.com/documentation/uikit/uidatadetectortypes
To make an entire view tappable:
You can add a UITapGestureRecognizer to the view (or not if it does not contain a phone number or hyperlink) like so:
https://stackoverflow.com/a/28675664/7270113
This answer may not be using the Swift version you are using. If so the compiler will tell you how to change it, so it will work.
If you don't like using Selectors, I recommend using the library Closures
https://github.com/vhesener/Closures, specifically with this https://vhesener.github.io/Closures/Extensions/UITapGestureRecognizer.html

Add default text in textfield with follow of user typing value

I have one textfield.Where user will enter some name .But i want to keep some default text in my textfield followed by user typed values.
Like below :
usertypetextwillcomehere ##samsn
So, the ##samsn will be the default text have to be in textfield. And the usertypetextwillcomehere will show the user typed value....in text field. If user type 2 letters also it have to show next to that two letter.Like below :
he ##samsn
h ##samsn
seconstextwillgere ##samsn
Its should follow with what ever user type value..Any idea that will be helpfull thanks
Use this UITextField subclass, configure your postfix, and enjoy
improved
class PostFixTextField: UITextField {
#IBInspectable var postfix : String = ""
#IBInspectable var removePostfixOnEditing : Bool = true
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.addTarget(self, action: #selector(textHasChanged), for: .editingDidEnd)
self.addTarget(self, action: #selector(removePostFix), for: .editingDidBegin)
self.addTarget(self, action: #selector(textHasChanged), for: .editingChanged)
}
func textHasChanged()
{
self.removePostFix()
self.addPostFix()
self.setCursorPosition(input: self, position: (self.originalText()?.characters.count)!)
}
private func setCursorPosition(input: UITextField, position: Int) {
let position = input.position(from: input.beginningOfDocument, offset: position)!
input.selectedTextRange = input.textRange(from: position, to: position)
}
func addPostFix()
{
if(self.text != nil)
{
self.text = self.text! + postfix
}
}
func originalText() ->String?{
let prefixRange = NSString(string: (self.attributedText?.string)!).range(of: postfix)
if(prefixRange.location != NSNotFound)
{
return self.text!.replacingOccurrences(of: postfix, with: "")
}
return self.text
}
func removePostFix(){
if(self.removePostfixOnEditing && self.text != nil)
{
self.text = self.originalText()
}
}
}
Hope this helps you
You could put a label behind the text field and make the text field's background transparent. When the text field value updates, you can update the label's text with ##samsn added to the end. If you get the font and size exactly the same it will look like it's part of the text field.

Why Is My IBDesignable UIButton Subclass Not Rendering In Interface Builder?

In the screen shot below please note that the DropDownButton (the selected view) is not being live rendered. Also, please note the "Designables Up To Date" in the Identity Inspector. Finally, please note the two break points in the assistant editor: if I execute Editor -> Debug Selected Views then both of these break points are hit.
Here's what It looks like when I run it:
Here's the code:
#IBDesignable
class DropDownButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
if image(for: .normal) == nil {
//setImage(#imageLiteral(resourceName: "DropDown"), for: .normal)
let bundle = Bundle(for: DropDownButton.self)
if let image = UIImage(named: "DropDown", in: bundle, compatibleWith: nil) {
setImage(image, for: .normal) // Editor -> Debug Selected Views reaches this statement
}
}
if title(for: .normal) == nil {
setTitle("DropDown", for: .normal) // Editor -> Debug Selected Views reaches this statement
}
addTarget(self, action: #selector(toggle), for: .touchUpInside)
}
override func layoutSubviews() {
super.layoutSubviews()
if var imageFrame = imageView?.frame, var labelFrame = titleLabel?.frame {
labelFrame.origin.x = contentEdgeInsets.left
imageFrame.origin.x = labelFrame.origin.x + labelFrame.width + 2
imageView?.frame = imageFrame
titleLabel?.frame = labelFrame
}
}
override func setTitle(_ title: String?, for state: UIControlState) {
super.setTitle(title, for: state)
sizeToFit()
}
public var collapsed: Bool {
return imageView?.transform == CGAffineTransform.identity
}
public var expanded: Bool {
return !collapsed
}
private func collapse() {
imageView?.transform = CGAffineTransform.identity
}
private func expand() {
imageView?.transform = CGAffineTransform(rotationAngle: .pi)
}
#objc private func toggle(_: UIButton) {
if collapsed { expand() } else { collapse() }
}
}
First Edit:
I added the prepareForInterfaceBuilder method as per #DonMag's answer. Doing so made an improvement but there is still something wrong: Interface builder seems confused about the frame. When I select the button only the title is selected, not the image (i.e. triangle). I added a border; it goes around both the title and the image. Here is a picture:
If I drag the button to a new position then everything moves, title and image.
Also, it surprised me that prepareForInterfaceBuilder made a difference. My understanding of this method it that it allows me to do interface builder only setup such as providing dummy data.
Add this:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
initialize()
}

Resources