I am currently developing an application which has two UILabels inside a Vertical StackView, two UITextFields inside a Vertical StackView, and both of those Vertical StackViews inside one Horizontal StackView as such:
I have constraints put in place. When the application runs on bigger devices such as an iPhone 11, it looks perfect as you can see here:
But if I switch to a smaller device like the iPhone 8 you can see the lines hug the edge of the phone as such:
The way I make the underline for the TextField is by using a class I created called StyledTextField. It looks like this:
class StyledTextField: UITextField {
var bottomLine = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
styleTextField()
}
private func styleTextField() {
font = UIFont(name: "Quicksand", size: UIFont.labelFontSize)
// Create the bottom line
bottomLine.frame = CGRect(x: 0, y: frame.height - 2, width: frame.width, height: 2)
bottomLine.backgroundColor = Environment.Colours.primary.cgColor
// Remove border on text field
borderStyle = .none
// Add the line to the text field
layer.addSublayer(bottomLine)
}
func makeUnderlineLight() {
bottomLine.backgroundColor = Environment.Colours.primaryAssisted.cgColor
}
}
In the Storyboard, I assign the UITextField class to be that of the "StyledTextField".
Additionally, in my viewDidLoad function on the Controller that deals with the UITextFields, I call the setUpUI() function which does the following:
func setUpUI() {
title = "Add Task"
taskNameTextField.attributedPlaceholder = NSAttributedString(string: "Name", attributes: [NSAttributedString.Key.foregroundColor: Environment.Colours.lightGray])
moreInfoTextField.attributedPlaceholder = NSAttributedString(string: "(Optional)", attributes: [NSAttributedString.Key.foregroundColor: Environment.Colours.lightGray])
setUpDatePicker()
moreInfoTextField.makeUnderlineLight()
if isEditing() {
showTaskToEdit()
}
view.backgroundColor = Environment.Colours.secondary
}
As you can see, I call the makeUnderlineLight() StyledTextField function once inside there.
Thank you!
Simple two-step solution:
Rewrite styleTextField so that it keeps a reference to the underline layer and removes the underline layer if it already exists, before making a new one.
Move the call to styleTextField() to layoutSubviews. (Don't forget to call super.)
select Your "More Info:" label and set horizontal compress resistanse priority to 1000
it ca be found here:
Related
I am trying to set an underline on my UITextFields. I have tried a couple of methods but none of them seem to work. After looking through a couple of websites, the most suggested method is the following:
extension UITextField {
func setUnderLine() {
let border = CALayer()
let width = CGFloat(0.5)
border.borderColor = UIColor.lightGray.cgColor
border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width-10, height: self.frame.size.height)
border.borderWidth = width
self.layer.addSublayer(border)
self.layer.masksToBounds = true
}
}
I can't think of any reason as to why the code above would not work, but all the answers I saw were posted a couple of years ago.
Could someone please let me know what I am doing wrong?
One problem I see with the code that you posted is that it won't update the layer if the text field gets resized. Each time you call the setUnderLine() function, it adds a new layer, then forgets about it.
I would suggest subclassing UITextField instead. That code could look like this:
class UnderlinedTextField: UITextField {
let underlineLayer = CALayer()
/// Size the underline layer and position it as a one point line under the text field.
func setupUnderlineLayer() {
var frame = self.bounds
frame.origin.y = frame.size.height - 1
frame.size.height = 1
underlineLayer.frame = frame
underlineLayer.backgroundColor = UIColor.blue.cgColor
}
// In `init?(coder:)` Add our underlineLayer as a sublayer of the view's main layer
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(underlineLayer)
}
// in `init(frame:)` Add our underlineLayer as a sublayer of the view's main layer
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(underlineLayer)
}
// Any time we are asked to update our subviews,
// adjust the size and placement of the underline layer too
override func layoutSubviews() {
super.layoutSubviews()
setupUnderlineLayer()
}
}
That creates a text field that looks like this:
(And note that if you rotate the simulator to landscape mode, the UnderlineTextField repositions the underline layer for the new text field bounds.)
Note that it might be easier to just add a UIView to your storyboard, pinned to the bottom of your text field and one pixel tall, using your desired underline color. (You'd set up the underline view using AutoLayout constraints, and give it a background color.) If you did that you wouldn't need any code at all.
Edit:
I created a Github project demonstrating both approaches. (link)
I also added a view-based underline to my example app. That looks like this:
I want to create custom view like below.
As you see, it consists from title and price labels. Title can have million number of lines, but its top edge should be aligned with price label. It seems simple design, but it has hundreds of solution. I tried every of them, but my title label is not growing, by having dots at the end (numberOfLines = 0 doesn't help). Here is how I approached to create such a design:
I created titleLabel with top, leading, trailing to price label, bottom constraints. Also, I created price label with top and trailing constraints only in order to align their top edges. I assigned compression resistance and hugging priority to price label, because it is more important and should not be ruined. Here is code if you want:
addSubview(titleLabel)
addSubview(priceLabel)
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.trailing.lessThanOrEqualTo(priceLabel.snp.leading).offset(-8)
make.top.equalToSuperview()
make.bottom.equalToSuperview()
}
priceLabel.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-16)
make.top.equalTo(titleLabel.snp.top)
}
I created separate custom view, because I want to use it inside StackView(spacing 8, distribution fill, vertical). Result of this approach: title label's is not growing. It has only one line with dots at the end, if it has big text.
Second approach was to create stackView (horizontally, spacing 8, distribution fill, alignment top). I set alignment top in order to align top edges of the labels. The result was the as in approach #1.
How to solve this problem? Where am I wrong? It seems I don't see something core in Auto Layout theory here.
Add a new height constraint as well for the title label with
relation: greater than equal to
contant: some constant(might be 20 or something based on your fontsize and content).
I hope this solves your issue
During development of your layout, it can be very helpful to use contrasting colors for element backgrounds... makes it really easy to see what's happening with their frames.
Give this a try...
Custom view class
class NeoCustomView: UIView {
let titleLabel = UILabel()
let priceLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.numberOfLines = 0
titleLabel.font = .systemFont(ofSize: 17)
priceLabel.translatesAutoresizingMaskIntoConstraints = false
priceLabel.font = .boldSystemFont(ofSize: 17)
priceLabel.setContentHuggingPriority(.required, for: .horizontal)
priceLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
addSubview(titleLabel)
addSubview(priceLabel)
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.trailing.lessThanOrEqualTo(priceLabel.snp.leading).offset(-8)
make.top.equalToSuperview()
make.bottom.equalToSuperview()
}
priceLabel.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-16)
make.top.equalTo(titleLabel.snp.top)
}
// use some background colors so we can easily see the frames
backgroundColor = .red
titleLabel.backgroundColor = .yellow
priceLabel.backgroundColor = .green
}
}
Example view controller class - adds another label constrained 4-pts from the bottom of the custom view so we can see everything working:
class NeoViewController: UIViewController {
let testView = NeoCustomView()
let anotherLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
anotherLabel.translatesAutoresizingMaskIntoConstraints = false
anotherLabel.font = .systemFont(ofSize: 15)
anotherLabel.backgroundColor = .blue
anotherLabel.textColor = .white
anotherLabel.textAlignment = .center
anotherLabel.numberOfLines = 0
anotherLabel.text = "This label is constrained 4 points from the bottom of the custom view."
view.addSubview(testView)
view.addSubview(anotherLabel)
testView.snp.makeConstraints { make in
make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
make.top.equalTo(view.safeAreaLayoutGuide).offset(40)
}
anotherLabel.snp.makeConstraints { make in
make.top.equalTo(testView.snp.bottom).offset(4)
make.width.equalTo(testView.snp.width)
make.centerX.equalTo(testView.snp.centerX)
}
testView.titleLabel.text = "This is long text for the title label that will word wrap when it needs to."
testView.priceLabel.text = "300$"
}
}
Result (Red is custom view with title and price labels, Blue is a label added and constrained below the custom view):
I'm developing an app with Swift 4 and targeting iOS 9 and above. For a view I have establish a UIScrollView as in the following post:
https://medium.com/#pradeep_chauhan/how-to-configure-a-uiscrollview-with-auto-layout-in-interface-builder-218dcb4022d7
So my test app looks like this:
Storyboard status corresponds to the final step of the link above
My project is cocoapods based so I added FlexLayout and PinLayout libraries for custom layout my UIView. I added custom UIViews programmatically inside the view container of the scrollview (like cards) and for each UIView when they are instantiated I attach a UITapGestureRecognizer to such instance.
ViewController:
func drawRows() {
flexContainer.flex.direction(.column).define {
(mainFlex) in
for i in 0..<15 {
let newCard = EmptyCard(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
newCard.backgroundColor = .lightGray
let label = UILabel(frame: CGRect(x: 10, y: 10, width: 100, height: 50))
label.text = "Card: \(i)"
newCard.addSubview(label)
mainFlex.addItem(newCard).marginLeft(10).marginBottom(5).width(200).height(100)
}
}
contentView.addSubview(flexContainer)
flexContainer.pin.all()
flexContainer.flex.layout(mode: .adjustHeight)
scrollView.contentSize = flexContainer.frame.size
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
drawRows()
}
Each card has the next class definition:
import UIKit
class EmptyCard: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
addGestureToCard()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addGestureToCard()
}
func addGestureToCard() {
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleOnTap))
gesture.cancelsTouchesInView = false
self.addGestureRecognizer(gesture)
self.isUserInteractionEnabled = true
}
#objc func handleOnTap(sender : UITapGestureRecognizer) {
print("Hola Mundo")
}
}
The result is the following:
resulting UI
The problem comes when I tap on a UIView that is not showed at first sight because it was added in a coordinate of the UIScrollView that is far from the device dimensions, for example the cards 6 and above the tap gesture recognizer doesn't work
cards that don't have the gesture recognizer active
For the rest of the UIViews (the ones that fit initially within the device dimensions, i.e., Card 0, Card 1, Card 2, Card 3, Card 4 and the top of Card 5) the gesture recognizer works perfectly, i.e., the "Hola Mundo" string message is shown in the debug console. I tried to fix the problem giving fixed dimensions to the view container (the one inside of the scroll view) but that did't work. I hope that somebody has a clue for this scenario, thank's in advance.
I added a left view to my UITextField via:
customTextField.leftView = searchIconView
customTextField.leftViewMode = .always
However, I later need to remove that icon from my customTextField and make it look like the original. You would think that you could just do this and it would reset:
customTextField.leftView = nil
customTextField.leftViewMode = .never
This did not work. It got rid of my search icon, but the padding of the left view still acted like it was there. It just is this weird whitespace.
I figured out the answer to my own question:
I had to leave the customTextField.leftViewMode as .always once I had set it. Then, when I want to revert back to the original, I just set a new view for the Left View that is zero everything except has the original padding for the customTextField.
customTextField.leftView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: customTextField.originalLeftPadding, height: 0)))
I get the original text field padding by subclassing UITextField for my customTextField, then I just hold onto the original left padding as a variable:
class CustomTextField : UITextField {
var originalLeftPadding: CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
originalLeftPadding = leftPadding
}
}
So i am using a custom function to format an subview that I am adding to a UICollectionViewCell. It is from Brian Voong's public project here: https://github.com/purelyswift/facebook_feed_dynamic_cell_content/blob/master/facebookfeed2/ViewController.swift.
func addConstraintsWithFormat(format: String, views: UIView...) {
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerate() {
let key = "v\(index)"
viewsDictionary[key] = view
view.translatesAutoresizingMaskIntoConstraints = false
}
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
What is interesting, is that in my UICollectionView I add a SubView to a single cell, and set the background color to white. The background is white when I comment out the line which sets the background for the subview, and no background color is set when I uncomment out the line setting the visually formatted constraints for the subview.
Here are the two lines which clobber each other:
func chronicleOneClicked(sender: UIButton) {
point1view.backgroundColor = UIColor.whiteColor()
addSubview(point1view)
//When the below is commented the background of point1view disappears
//addConstraintsWithFormat("|-50-[v0]-50-|", views: point1view)
}
when I do print(subviews) i see that the UIView with the white background color is the highest in the view stack (top of the stack). When i print out subviews[subviews.count-1].backgroundColor I get the Optional(UIDeviceWhiteColorSpace 1 1) which is what I expect. it is strange because the color is not displayed.
I am not sure how to go about seeing what is happening behind the scenes to confirm that the background is being set at all in the latter case.
This all happens in a class for the UiCollectionViewCell which I am using as the class of one of my UICollectionView Cells which can be viewed in its entirety here:
https://gist.github.com/ebbnormal/edb79a15dab4797946e0d1f6905c2dd0
Here is a screen shot from both cases, the first case is where the line addConstraintsWithFormat is commented out, and the second case is where it is uncommented: The subview of point1subview is highlighted with a white background in the first case.
This is how I setup the views. It all happens in a class that overrides UICollectionViewCell
class myClass : UICollectionViewCell {
var chronicle: BrowsableChronicle? {
didSet{
//etc.
point1.addTarget(self, action: #selector(chronicleOneClicked(_:)), forControlEvents: UIControlEvents.TouchUpInside)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
let point1 : PointButtonView = {
let pointView = PointButtonView(frame: CGRectMake(0, 0, 25, 25 ))
return pointView
}()
//NOTE here is where I create the view, whose background doesn't display
let point1view : UIView = {
let pointView = UIView(frame: CGRectMake( 0, 0, 200, 270))
pointView.backgroundColor = UIColor.whiteColor()
let title = UILabel(frame: CGRectMake(0, 0, 200, 21))
title.font = UIFont(name:"HelveticaNeue-Bold", size: 16.0)
pointView.addSubview(title)
let summary = UILabel(frame: CGRectMake(0, 0, 190, 260))
summary.lineBreakMode = NSLineBreakMode.ByWordWrapping
summary.numberOfLines = 4
summary.font = UIFont(name:"HelveticaNeue", size: 12.5)
pointView.addSubview(summary)
let button = UIButton(frame: CGRectMake(0, 200, 190, 30))
button.backgroundColor = UIColor(red:0.00, green:0.90, blue:0.93, alpha:1.0)
pointView.addSubview(button)
pointView.tag = 100
return pointView
}()
//NOTE: here is where I add the subview to the UICollectionViewCell view
func chronicleOneClicked(sender: UIButton){
addSubview(point1view)
addConstraintsWithFormat("H:|-20-[v0]-20-|", views: point1view)
//TODO anytime i add a constraint here the background color leaves!
print(subviews[subviews.count-1].backgroundColor) //Prints white
}
}
UPDATE: I thought maybe it was related to this issue :
UITableViewCell subview disappears when cell is selected
Where the UICollectionViewCell is selected, and therefore iOS automatically sets the backgroundColor to clear. The problem is, that I implemented this class extension of UIView to see when didSet is called on the backgroundColor and when it is set to clear, i set it to white. However, it only calls didSet on the backgroundColor once, when i first set the color of the view. Here is the code I used to override the UIView class:
class NeverClearView: UIView {
override var backgroundColor: UIColor? {
didSet {
print("background color is being set")
if backgroundColor == UIColor.clearColor() {
print("set to a clear color")
backgroundColor = UIColor.whiteColor()
}
}
}
}
The difference you are seeing is obviously caused by a view frame resulting in zero width or zero height.
Let's explain how the drawing system works.
Every view has a layer that draws its background color in its bounds, which are specified by the view frame. Then every subview is drawn. However, the subviews are not limited by the frame unless you set UIView.clipsToBounds to true.
What you are seeing means the a container view has a zero frame (either width or height) but its subviews have correct frame, therefore they are displayed correctly.
There are multiple reasons why this could happen, for example:
You are setting translatesAutoresizingMaskIntoConstraints to false to some system view (e.g. the content view of the UICollectionView).
You have a constraint conflict, resulting in some important constraint to be removed (you should see a warning).
You are missing some constraints. Specifically, I don't see you setting vertical constraints.
You should be able to debug the problem using the view debugger in Xcode. Just open your app, click the view debugger button and print the recursive description of the cell. You should see a frame that is zero.