ios autolayout: dynamic width should push down 2nd label with SnapKit, proper way - ios

I have 2 UILabels. lbl1's text can change, while lbl2's text is static.
They are both initially set on the same line. I have lbl1's numberOfLines set to 0 (infinity). I would like lbl2 to push down and align its top to lbl1's bottom when lbl1's width encroaches on lbl2's space. I have a current solution, but feel there's a pure autolayout way I'm missing. Any help would be great!
Both labels fit on 1 line:
lbl2 does not shift down in my attempts at pure autolayout:
Desired:
Current solution works, but feels un-ideal:
import XCPlayground
import SnapKit
import UIKit
let screen: UIView = UIView(frame: CGRectMake(0,0,320,568))
XCPlaygroundPage.currentPage.liveView = screen
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
let lbl1: UILabel = UILabel()
lbl1.text = "Short Text asnoetuha 1 1 1 1 "
lbl1.numberOfLines = 0
lbl1.backgroundColor = UIColor.whiteColor()
let lbl2: UILabel = UILabel()
lbl2.text = "Amount Text"
lbl2.backgroundColor = UIColor.yellowColor().colorWithAlphaComponent(0.5)
screen.addSubview(lbl1)
screen.addSubview(lbl2)
lbl1.snp_makeConstraints { (make: ConstraintMaker) in
make.top.leading.equalTo(screen)
make.width.lessThanOrEqualTo(screen.snp_width)
}
lbl2.snp_makeConstraints { (make: ConstraintMaker) in
// Below lines feel un-ideal
let lbl2X: CGFloat = screen.frame.width - lbl2.intrinsicContentSize().width
let lbl1RightX: CGFloat = screen.frame.origin.x + lbl1.intrinsicContentSize().width
let hasOverlap: Bool = lbl1RightX >= lbl2X
make.top.equalTo(hasOverlap ? lbl1.snp_bottom : lbl1.snp_top).priorityLow()
make.trailing.equalTo(screen)
}

I do believe you are not approaching this the right way. You should be able to create a function that listens for when lbl1 has its right anchor come into contact with lbl2's left anchor (very easy with SnapKit).
When this does happen, you call a function that remakes the constraints to how fit the image you provided. This is done with a call to snap_updateConstraints or snp_remakeConstraints. Then your constraint code should be straight forward as you would just pin the bottom anchor of lbl1 to the top anchor of lbl2 and then just make the other constraints until you get that desired effect.

Related

Wrapping text on TitleLabel and DetailLabel - Material Card

I am using CosmicMind - Material Framework to create a Card. titleLabel and DetailLabel do not wrap to a new line.
My question is: What do I need to do to accomplish such task and is it supposed to be automatically adjusted by the framework?
This is what I have so far:
let card = Card()
var heartIcon = IconButton()
heartIcon = IconButton(image: Icon.favoriteBorder, tintColor: Color.red.base)
//Title Bar
let toolbar = Toolbar(rightViews: [heartIcon])
toolbar.title = cardData.cardTitleText
toolbar.titleLabel.textAlignment = .left
toolbar.titleLabel.numberOfLines = 0
toolbar.detail = "Company: " + cardData.cardTitleSubtitle
toolbar.detailLabel.font = RobotoFont.regular(with: 14)
toolbar.detailLabel.textColor = Color.grey.base
toolbar.detailLabel.textAlignment = .left
toolbar.detailLabel.numberOfLines = 0
And this is my output:
Update:
I managed to achieve what I wanted by increasing the size of the toolbar frame.
toolbar.frame = CGRect(x:0,y:0,width: view.frame.width,height: 100)
My goal was to at least show 2 lines of text.
Thanks!
I have no experience with the CosmicMind framework but usually, the label.numberOfLines property defines the maximum number of lines that the label text can have. You can set it to 2 instead of 0 and it should wrap.

UILabel on second line if the content doesn't fit in the view's first line

The best way to describe my situations is with images. What I have is a view which contains several UILabels and UIImage. The red box is a UILabel and if the content is too big it should go to the second line.
From the storyboard I have a working case when the content fits but the problem is that I am not sure how to handle the case when the last (red box) should go to the second line. I am using autolayout and cartography.
If someone can point me to the right direction I will be very grateful.
First calcululate width of text as per your current label's position.
If text width is more than current label's width then see my answer from below link:
Auto Layout how to move the view2 from right to the bottom?
Calculate width:
func widthForView1(_ text:String, font:UIFont, height:CGFloat) -> CGFloat
{
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: your_label_width, height: your_lable_height))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.text = text
label.font = font
label.sizeToFit()
return label.frame.width
}
You cannot do that with constraints only. To change the entire position of the element on the screen, you need to do it programmatically.
Use of tag View can solve this issue. TagListView is an external library.
When u add a view as subclass of taglistView, its height automatically increases accordingly.
ADD this to pod file : pod 'TagListView'
func addTags() {
let str1 = "Hi"
tagListView.addTag(str1)
let str2 = "Helloo"
tagListView.addTag(str2)
let str3 = "How Are u ? "
tagListView.addTag(str2)
tagListView.isUserInteractionEnabled = false
}

How to center the subviews of UIScrollView

I'm a beginner in creating a custom view. I'm trying to create a custom UIView with a scrollview and buttons that will look like this:
I'm adding a view(view with label of page number) inside of scrollView depending on the the number of pages. Is that how it should be?
Currently it looks like this:
My question is how can I center the subviews of scrollview? and next is what's wrong with this code? Why is that I can only see 1 label inside the view? and the other doesn't show up. How can I scroll to the selected page if the page number is not visible already in the scrollview?
Here's my code:
func addPageNumberViewWithCount(count: Int) {
var pageNumberViewX: CGFloat! = 0
let pageNumberViewDistance: CGFloat! = 10
for i in 1...count {
let pageNumberView = UIView(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
pageNumberView.backgroundColor = UIColor.redColor()
pageNumberView.layer.cornerRadius = pageNumberView.frame.height / 2
pageNumberView.layer.masksToBounds = true
pageNumberView.clipsToBounds = true
// add number label
let label = UILabel(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
label.center = pageNumberView.center
label.text = "\(i)"
label.textAlignment = .Center
pageNumberView.addSubview(label)
// update x for next view
pageNumberViewX = pageNumberView.frame.origin.x + pageNumberView.frame.width + pageNumberViewDistance
// add view inside scrollview
scrollView.addSubview(pageNumberView)
if i == count {
scrollView.contentSize = CGSizeMake(pageNumberViewX + pageNumberView.frame.width, 30)
}
}
}
Part of my answer will go to providing a solution to your question,and another part of my answer will go toward strongly suggesting that this not be the method you use to complete your desired tasks.
At this point, AutoLayout and Interface Builder have come a long way. Where they used to be difficult to use because of their inconsistency and unpredictability, they are now highly predictable and consistent as long as you understand the tools and how to use them.
Apple's suggested method for completing this task (which I mostly stand behind) is creating a .xib file (nib) to lay out the base components of the design, and to load the nib into the view or view controller whenever that design should be used. My question for you: have you tried this, or have you determined for some reason that this would be an unsatisfactory solution to your problem? AutoLayout exists to solve these problems not just in allowing you to achieve your desired solution in this one situation but to achieve it in other situations as well, with varying screen sizes and device types.
Now, if you were to simply ignore all of that and continue on your path, there would be a few good ways to handle your problem. One suggested solution I have:
1) Wrap your pageNumberView in another view. Constrain that view to the size of the scrollView. Doing this gives the scrollView content with which to base its scrollable content size, and gives the inner pageNumberView something to compare itself to.
2) Center the pageNumberView horizontally in its container (the new view that we just created).
Doing this, the page numbers should now center themselves in the container until they reach a size where they exceed the width of the scrollView. At that point, they will then continue to expand, making the area horizontally scrollable.
I can provide code examples of how you would do this, but frankly I would much prefer if you scrapped the idea of doing things this way and instead opted for the AutoLayout method at least, and perhaps even the Interface Builder method. I started out with iOS the same way you did, trying to do everything in code. It really isn't the best way to do things, at least with regard to iOS.
Edit: I've provided an example of how this would look in Interface Builder using UINib. I've populated the view with an example of 5 pages to show what it is like. I will see if I can make a GIF or something similar to show what each of the subviews look like.
For the OP, my suggestion would be this: Use this for reference, and go learn the constraints system. It is extremely unlikely that you will find success with iOS if you do not learn and utilize the constraints system. Coding in X values to a UIView's frame is only going to create a product with poor, inconsistent performance across devices, and will take much, much longer than it would to take the time to learn constraints.
Perhaps you should have a UICollectionView with a cell for each of these buttons. That's a better way of doing this, and you can lay it out again when the screen rotates and it changes width.
Those cells will layout offset to the left. You can solve that this way:
let pageNumberViewTotalWidth = 30 * count + (pageNumberViewDistance * count - 1)
self.collectionView.contentInset.left = (self.collectionView.frame.size.width - pageNumberViewTotalWidth) / 2
The labels aren't showing up because you're setting their frame's x to be the same as the page number view's x. It's frame should be relative to it's superview, in this case pageNumberView.
First Question of yours "how can I center the subviews of scrollview?"
Solution: lets suppose you have in total 50 pages and you want to show 5 pages at a time in the scrollview.
Then make 10 subviews of equal widths where each subview width will be equal to visible portion of the collection view that is
self.view.size.width - 2*(width of toggle button)
Then in each container view add 5 of your pageNumberView placed at equal distance
lets pageNumberViewWidth = container.width/5 - 2*margin
now pageNumberView frame will be (margin,0,pageNumberViewWidth,height)
In this way in each container view your pageNumberViews will be placed equally and it will look as if you have centred them.
Second Question "Why is that I can only see 1 label inside the view?"
Answer : Its because you are setting label frame incorrectly
let label = UILabel(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
Here label is the subview of pageNumberView So you have to set its frame according to its parent's view which is pageNumberView, so change it to
let label = UILabel(frame: CGRectMake(0, 0, 30, 30))
First time it was right because pageNumberViewX is 0 for first iteration after that it become some positive value which makes its frame shifted to right but its parent's width is small so its not visible to you.
Third Question : "How can I scroll to the selected page if the page number is not visible already in the scrollview?"
For this you need to find the frame of your selected page:
you can do that by using the offset that you used to create pageNumberView.
(width of each pageNumberView)*pageNumber = starting point of the required pageNumberView.
let frame : CGRect = CGRectMake(calculated offset above, 0,30, 30)
//where you want to scroll
self.scrollView.scrollRectToVisible(frame, animated:true)
I hope this will help you in solving your problem
Edit for first problem
func addPageNumberViewWithCount(count: Int) {
var containerViewX: CGFloat! = 0
let pageNumberViewDistance: CGFloat! = 10
let pageNumberViewPerSubview = 5
var numberOfSubview = count/pageNumberViewPerSubview
if(count % pageNumberViewPerSubview > 0){
numberOfSubview = numberOfSubview + 1
}
var pagesLeft = count
for i in 1...numberOfSubview {
var pageNumberViewX: CGFloat! = 0
let containerView : UIView = UIView(frame:CGRectMake(containerViewX,0,scrollView.frame.size.width,scrollView.frame.size.height))
if(pagesLeft < pageNumberViewPerSubview){
for k in 1...pagesLeft{
}
}
else{
for j in 1...pageNumberViewPerSubview{
let pageNumberView = UIView(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
pageNumberView.backgroundColor = UIColor.redColor()
pageNumberView.layer.cornerRadius = pageNumberView.frame.height / 2
pageNumberView.layer.masksToBounds = true
pageNumberView.clipsToBounds = true
// add number label
let label = UILabel(frame: CGRectMake(0, 0, 30, 30))
label.text = "\(i)"
label.textAlignment = .Center
pageNumberView.addSubview(label)
// update x for next view
pageNumberViewX = pageNumberView.frame.origin.x + pageNumberView.frame.width + pageNumberViewDistance
containerView.addSubview(pageNumberView)
}
containerViewX = containerViewX + scrollView.frame.size.width
// add view inside scrollview
scrollView.addSubview(containerView)
pagesLeft = pagesLeft - pageNumberViewPerSubview
}
if i == count {
scrollView.contentSize = CGSizeMake(numberOfSubview*scrollView.frame.size.width, 30)
}
}
}

ios autolayout dynamic UILabel height

Say I have three UILabels whose positions are like below:
[Label1] [Label2]
[Label3]
Label1 and Label2 are in the same row and Label3 is below them. All the labels will have a fixed width and will contain dynamic text, so their height will vary.
How do I make the Label3 10 points below the label which has a higher height using AutoLayout?
For example, if Label1's height is 100 points, Label2's height is 120 points (their Y positions are the same), then Label3 should be 10 points below Label2, but if Label1 is 120 points high and Label2 is 100 points high, then Label3 should be 10 points below Label1.
You simply make constraints between both Label3->Label1 and Label3->Label2. Use inequality constraints. There will be only one way to satisfy both!
You will also need a top constraint for Label3; its constant should be very small and its priority should be very low. This will give the two inequality constraints something to "aim at".
Here is an example. This as achieved entirely without code - the buttons have code to add text to the labels, of course, but the constraints are configured entirely in Interface Builder; the labels are resizing, and the bottom label is moving down, automatically. (You can construct the same layout in code if you want to, naturally.)
I suggest you to wrap top two labels to UIView and setup constraints so these labels fit all space inside that view. Then you simple add vertical spacing constraint to bottom label3 with constant = 10. In that case top view will have size of larger label and will satisfy your conditions
I thought this would be an interesting exercise so I create a little test project. The gist of the code is below. You can just copy/paste it in the standard Single View iOS template.
(Note that I use SnapKit for programmatic Auto Layout because it is so much simpler than the UIKit API. I find it even much simpler than doing things in Xcode.)
The result is exactly the same as Matt's great screencast.
// ViewController.swift
import UIKit
import SnapKit
class ViewController: UIViewController
{
override func viewDidLoad() {
super.viewDidLoad()
let leftLabel = UILabel()
leftLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "addText:"))
leftLabel.userInteractionEnabled = true
view.addSubview(leftLabel)
leftLabel.numberOfLines = 0
leftLabel.text = "All the world's a stage, and all the men and women merely players: they have their exits and their entrances; and one man in his time plays many parts, his acts being seven ages."
leftLabel.snp_makeConstraints { (make) -> Void in
make.top.equalTo(40)
make.left.equalTo(self.view)
make.right.equalTo(self.view.snp_centerX)
}
let rightLabel = UILabel()
rightLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "addText:"))
rightLabel.userInteractionEnabled = true
view.addSubview(rightLabel)
rightLabel.numberOfLines = 0
rightLabel.text = "There is a tide in the affairs of men, Which taken at the flood, leads on to fortune. Omitted, all the voyage of their life is bound in shallows and in miseries. On such a full sea are we now afloat. And we must take the current when it serves, or lose our ventures."
rightLabel.snp_makeConstraints { (make) -> Void in
make.top.equalTo(40)
make.right.equalTo(self.view)
make.left.equalTo(self.view.snp_centerX)
}
let bottomView = UIView()
view.addSubview(bottomView)
bottomView.backgroundColor = UIColor.redColor()
bottomView.snp_makeConstraints { (make) -> Void in
make.height.equalTo(20)
make.left.right.equalTo(self.view)
make.top.greaterThanOrEqualTo(leftLabel.snp_bottom)
make.top.greaterThanOrEqualTo(rightLabel.snp_bottom)
}
}
#objc func addText(recognizer: UIGestureRecognizer) {
if let label = recognizer.view as? UILabel {
label.text = label.text! + " I like cheese."
}
}
}
Updated the code to add some additional text to the labels when tapped.
First of all remove height constraints and set all 3 labels vertical Content Compression Resistance Priority to 1000. This is the most important part.
Then add vertical space from Label3 to Label 1, and set instead of Equal, Greater Than or Equal with priority say 500. Add same space constraint to Label2.
Last add constraint from Label3 to Top = 0, but set priority to 1.

paged scrollView with labels

I'm trying to create a scrollView where u scroll between different labels. As an illustration i have this image:
I want to make the bottom scrollView i've tried creating a replicate, but in my case it is only adding "Book" to the scrollView and how can i make a smaller spacing between the labels. because in my code there are one label per self.view.frame.width
categoryArray = NSArray(objects: "Book", "Elektronik")
var textWidth = 0
for val in categoryArray!
{
var textLabel: UILabel = UILabel(frame: CGRectMake(0, CGFloat(textWidth), categoryScrollView!.frame.width, categoryScrollView!.frame.height))
textLabel.textAlignment = NSTextAlignment.Center
textLabel.text = val as NSString
categoryScrollView?.addSubview(textLabel)
textWidth = textWidth + Int(textLabel.frame.size.width)
if textWidth > Int(self.view.frame.width) {
categoryScrollView?.contentSize = CGSizeMake(CGFloat(textWidth), categoryScrollView!.frame.height);
}
}
I don't read swift very well, but it looks to me like the code is advancing the y position of the label frame on each iteration, rather than the x. In other words, change:
CGRectMake(0, CGFloat(textWidth), categoryScrollView!.frame.width, categoryScrollView!.frame.height)
to:
CGRectMake(CGFloat(textWidth), 0, categoryScrollView!.frame.width, categoryScrollView!.frame.height))

Resources