MDCChipField Swift - scrollable direction - ios

I am trying to use MDCChipField, the material design component for Swift.
I am implementing the 'input chip' type and am able to add the entered text as a chip with
let mdcSearchField = MDCSearchField()
mdcSearchField.addChip(chipView)
When the chips overflow in MDCChipField, they get added to next row. How can i set the scrollable direction to horizontal instead of vertical?
In the link,
https://material.io/design/components/chips.html#input-chips,
The placement section explains
Input chips can be integrated with other components. They can appear:
- Inline with the text input cursor in a field
- In a stacked list
- In a horizontally scrollable list
How do i do a a horizontally scrollable list in code?
Thanks.

My approach was this:
Create a scrollable view
create an offset of say 10 for padding between chips
Add chips to the scrollable view
Set the frame origin of the chip with the declared offset
Add the chip's width to the offset plus the initial offset value (in this case 10)
Increase the content width of the scrollable view, set the width to the total
offset
(add more chips/repeat)
On chip removal (if needed)
Set initial offset back to 10
Remove chip from parent scrollable view
Loop through subviews and set the offset again to each of them
Set the scrollable view new width using the total offset
Here is a sample in Swift
Configure initial layout
class ChipSample
{
var tagXOffset = 10
var tagPadding = 10
func configureTagsView()
{
tagView = UIScrollView(frame: CGRect(x: 0, y: 120, width: view.bounds.width, height: 40))
view.addSubview(tagView)
}
Add a chip
func addChip(name:String)
{
let chip = MDCChipView()
chip.titleLabel.text = name
chip.setTitleColor(.gray, for: .normal)
chip.sizeToFit()
chip.addTarget(self, action: #selector(removeChip), for: .touchUpInside)
tagView.addSubview(chip)
chip.frame.origin.x = tagXOffset
chip.frame.origin.y = 0
tagXOffset += tagPadding + chip.frame.width
tagView.contentSize = CGSize(width: tagXOffset, height: tagViewHeight)
}
Remove the chip
#objc func removeChip(sender: MDCChipView!)
{
tagXOffset = tagPadding
sender.removeFromSuperview()
for subview in tagView.subviews {
subview.frame.origin.x = tagXOffset
tagXOffset += tagPadding + subview.frame.width
}
}

Related

Is it possible to set the alignment of segmented Control titles to the left?

I have been looking around for a way to set the alignment of the segmented control titles to the left but I don't seem to be able to achieve what I want.
I have created this little function to change the frame of the subviews of the segment control.
It works at first.
func modifyFrameOfSegment() {
for segment in segmentedControl.subviews {
guard segment.subviews.isNotEmpty else { return }
segment.contentMode = .left
for label in segment.subviews where label is UILabel {
label.frame = CGRect(x: 0, y: label.frame.origin.y, width: label.frame.size.width, height: label.frame.size.height)
(label as! UILabel).textAlignment = .left
}
}
}
But everytime I select a new segment it resets the frames of all the subviews and center align all the titles again.
Is there a way to achieve a permanent left alignment for the segment titles in a segmented control?
Any tips or advice would be greatly appreciated.
Thank you for your time.
Let's use this method
self.segmentedControl.setContentPositionAdjustment(UIOffset(horizontal: -20, vertical: 0), forSegmentType: .left, barMetrics: .default)
And you can do what you want (Of course, you can change the horizontal & vertical value by your needs). Here is the result:
Update:
There's apparently no way to set the alignment of the items, but you can fake it by adjusting the position of each individual item using setContentOffset(_ offset: CGSize, forSegmentAt segment: Int). Here's a kludgy example:
class LeftSegmentedControl: UISegmentedControl {
var margin : CGFloat = 10
override func layoutSubviews() {
super.layoutSubviews()
leftJustifyItems()
}
func leftJustifyItems() {
let fontAttributes = titleTextAttributes(for: .normal)
let segments = numberOfSegments - 1
let controlWidth = frame.size.width
let segmentWidth = controlWidth / CGFloat(numberOfSegments)
for segment in 0...segments {
let title = titleForSegment(at: segment)
setWidth(segmentWidth, forSegmentAt: segment)
if let t = title {
let titleSize = t.size(withAttributes: fontAttributes)
let offset = (segmentWidth - titleSize.width) / 2 - margin
self.setContentOffset(CGSize(width: -offset, height: 0), forSegmentAt: segment)
}
}
}
}
Here's what it looks like:
There are a few caveats:
This version sets the segments to all have equal width, which might not be what you want.
I used a fixed left margin of 10px because it seems unlikely that you'd want to vary that, but you can obviously change it or make it a settable property.
Just because you can do this doesn't mean you should. Personally, I don't think it looks great, and it suffers in the usability department too. Users expect segmented control items to be centered, and left-justifying the items will make it harder for them to know where to tap to hit the segment. That seems particularly true for short items like the one labelled "3rd" in the example. It's not terrible, it just seems a little weird.
Original answer:
UIControl (of which UISegmentedControl is a subclass) has a contentHorizontalAlignment property that's supposed to tell the control to align its content a certain way, so the logical thing to do would be to set it like this:
let segmented = UISegmentedControl(items: ["Yes", "No", "Maybe"])
segmented.frame = CGRect(x:75, y:250, width:250, height:35)
segmented.contentHorizontalAlignment = .left
But that doesn't work — you still get the labels centered. If you've got a compelling use case for left-aligned segments, you should send the request to Apple.
One way you could work around this problem is to render your labels into images and then use the images as the segment labels instead of plain strings. Starting from the code in How to convert a UIView to an image, you could easily subclass UISegmentedControl to create images from the item strings.

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)
}
}
}

Swift 2.1 - Making dynamic buttons with width relative to text length

I need to make buttons programmatically based on the number of fetched values and each button needs to have width that is relative to their text length.
My current implementation has a fixed width with X position that also increments in fixed length.
What is the process of achieving this so I can have buttons like attached screenshot?
for var i = 0; i < self.category.count; i++ {
let frame1 = CGRect(x: 0 + (i * 45), y: 20, width: 80, height: 40 )
let button = UIButton(frame: frame1)
button.setTitle("\(category[i].name!)", forState: .Normal)
button.backgroundColor = UIColor.blackColor()
self.categoryScrollView.addSubview(button)
}
After you set text on button, use
button.sizeToFit()
then read the width of the button and add an appropriate number to it (this number will be same for all buttons regardless of the text length and will give you desired results.
Alternatively, you can add padding to button; in which case you only need to do the first step.

Xcode 6 Interface Builder centre multiple labels in middle of view

I'm writing an iOS app which has multiple labels in one view, like shown:
I would like these labels to be in the vertical centre of the view, with the middle of the collection of labels as the centre of the view.
I need to use auto layout for this, as the top label may be multiple lines, or may only be one depending on input, and will change height depending on this. This, along with the top label being a different size, means that I cannot simple have the middle label in the middle, and the others relative to that.
I'm looking for a solution either in code or IB.
EDIT: To clarify, I am looking to centre the middle of multiple labels, like so:
(The vertical middle might be slightly off)
*The image should read vertical middle
This is an old question of mine, but it maintains a fair number of views, and is quite a common use-case. I do not feel the other answer is a very efficient method to achieve this.
The easiest method to centre a collection of views is to place them within a UIView object which is itself centred.
To use the example above, the three UILabels would be within one UIView, with a 0 constraint between the top and bottom labels and the View. The view itself would then be set to be centred vertically.
I don't think there is a way to do this with Auto Layout, but you can code. I'm assuming you've already figured out the spacing of the labels, so I'll just help you center the whole thing.
var o1 = label1.frame.origin
var o4 = label4.frame.origin
var h4 = label4.frame.height
var w4 = label4.frame.width
var hc4 = o4.y + h4
var wc4 = o4.x + w4
var screen = UIScreen.mainScreen().bounds.size.height
var remainingScreen = (screen - (hc4 - o1.y))/2.0
var screenW = UIScreen.mainScreen().bounds.size.width
var remainingScreenW = (screenW - (wc4 - o1.x))/2.0
var moveH = remainingScreen - o1.y
var moveW = remainingScreenW - o1.x
var frame1 = label1.frame
var frame2 = label2.frame
var frame3 = label3.frame
var frame4 = label4.frame
label1.frame = CGRect(x: frame1.origin.x + moveW, y: frame1.origin.y + moveH, width: frame1.width, height: frame1.height)
label2.frame = CGRect(x: frame2.origin.x + moveW, y: frame2.origin.y + moveH, width: frame2.width, height: frame2.height)
label3.frame = CGRect(x: frame3.origin.x + moveW, y: frame3.origin.y + moveH, width: frame3.width, height: frame3.height)
label4.frame = CGRect(x: frame4.origin.x + moveW, y: frame4.origin.y + moveH, width: frame4.width, height: frame4.height)
This whole thing isn't very self explanatory, but I won't talk about what every line does. The overall idea is it finds the distance between the upper left and lower right corner of all the labels (so UL of label1 and LR of label4 [this only works if you've already set the spacing between labels and the width/height of each label]), then it finds the width/height (W/H) of the screen and subtracts the W/H of the label area, divides by two, giving the space between the top of the screen and the labels. Finally, it finds the distance the entire assembly needs to move by comparing the UL corner with where it should be, and combines the amount to be moved with the original location of all the labels.
Note: This code could be heavily condensed, it is just easier to view and read if it is written like this.

Autolayout two views vertically with dynamic height aligned top

I have two views that can be dynamic height.
Dependent on content either view1 or view2 is the highest.
The highest view should determinate the distance to view3.
In my current implementation view1 and view2 each have a constraint for x pixels for view3. But the problem is view1 and view2 then always will be same height (all the content will not align top)
Try this. Make sure both views 1 and 2, have some constraint to the top of the superview and to the sides (or fixed width, whatever it takes to fix the views in the horizontal direction). Lets say you want that distance to the bottom view to be 30. Give view 1 a constraint to view 3 of =30 with a priority of 200, and another one of >=30 with a priority of 1000 (default). View 2 should just need one constraint to view 3 of >=30 with 1000 priority.
The low priority constraint (of 200) will fix the position of view 3 initially, but is low enough that if view 2 expands, view 3 will move down, and also low enough that the default Content Hugging Priority of view 1 will keep it from expanding its height if it doesn't need to based on its text size.
swift
import SnapKit
view3.snp.makeConstraints {
$0.top.greaterThanOrEqualTo(view1.snp.bottom).offset(30)
$0.top.greaterThanOrEqualTo(view2.snp.bottom).offset(30)
$0.left.right.equalTo(0)
$0.height.equalTo(50)
}
There are two ways actually
With constraints (as described rdelmar)
class MyViewController: ViewController {
let view1 = View()
let view2 = View()
let view3 = View()
override func buildUI() {
super.buildUI()
body {
view1
.edgesToSuperview(top: 16, leading: 16)
view2
.edgesToSuperview(top: 16, trailing: -16)
.width(to: view1)
.leadingTo(view1, 16)
view3
.topTo(view1, 30 ! 200) // 200 is priority, ! is priority operator
.topTo(view2, >=30)
.edgesToSuperview(leading: 16, trailing: -16, bottom: -16)
}
}
}
I'm not sure here about =30 with 200 priority, maybe it should be >=30 as with view2, but you can easily figure it out with Live preview
With StackViews
class MyViewController: ViewController {
override func buildUI() {
super.buildUI()
body {
VStack {
HStack {
View() // view 1
View() // view 2
}
.distribution(.fillEqually) // to make view 1&2 width equally
.alignment(.top)
Space() // flexible space
VSpace(30) // fixed 30pt height space
View() // view 3
}
.edgesToSuperview()
}
}
}
All the examples are by using awesome UIKitPlus library
The accepted answer did not work for me (although it did put me on the right path). In my case, view 1 or view 2 could be any size and view 3 had to be below the highest one. This code got it to work, though I must say I find autolayout extremely unintuitive:
let view3TopAnchor1 = statusL.topAnchor.constraint(greaterThanOrEqualTo: view1.bottomAnchor, constant: 10.0)
view3TopAnchor1.priority = UILayoutPriority(100)
view3TopAnchor1.isActive = true
let view3TopAnchor2 = statusL.topAnchor.constraint(greaterThanOrEqualTo: view2.bottomAnchor, constant: 10.0)
view3TopAnchor2.priority = UILayoutPriority(100)
view3TopAnchor2.isActive = true
view1.setContentHuggingPriority(UILayoutPriority(101), for: .vertical)
view2.setContentHuggingPriority(UILayoutPriority(101), for: .vertical)
float yPos = MAX(view1.frame.size.height,view2.frame.size.height);
view3.frame.origin.y = view1.frame.origin.y + maxHeight;

Resources