How to animate Segmented Control selection? - ios

I have a UISegmentedControl with 3 possible selections and I have added a coloured underline bar to the first selection. I'm trying to figure out how to animate the underline so that it moves to highlight the new selection, but the underline bar is not moving.
I'm not looking to change the index with animation. I'm trying to figure out how to keep the underline bar UIView in the same segment that is selected. I've seen this implemented commonly.
Here's a screen recording of what's happening right now - https://imgur.com/a/jTQ8z1M
I added the UISegmentedControl via Storyboard and then added the underline in code with view.addSubview(underlineBar). Here's is what I have:
class ViewController: UIViewController {
#IBOutlet weak var segmentedControl: UISegmentedControl!
override func viewDidLoad() {
super.viewDidLoad()
setSegmentedControlStyle()
}
func setSegmentedControlStyle() {
segmentedControl.backgroundColor = .clear
segmentedControl.tintColor = .clear
segmentedControl.setTitleTextAttributes([NSAttributedStringKey.font : UIFont.systemFont(ofSize: 16), NSAttributedStringKey.foregroundColor: UIColor.lightGray
], for: .normal)
segmentedControl.setTitleTextAttributes([NSAttributedStringKey.font : UIFont.systemFont(ofSize: 16),
NSAttributedStringKey.foregroundColor: UIColor.blue
], for: .selected)
let underlineBar = UIView()
underlineBar.translatesAutoresizingMaskIntoConstraints = false // false since we are using auto layout constraints
underlineBar.backgroundColor = UIColor.blue
view.addSubview(underlineBar)
underlineBar.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor).isActive = true
underlineBar.heightAnchor.constraint(equalToConstant: 3).isActive = true
underlineBar.leftAnchor.constraint(equalTo: segmentedControl.leftAnchor).isActive = true
underlineBar.widthAnchor.constraint(equalTo: segmentedControl.widthAnchor, multiplier: 1 / CGFloat(segmentedControl.numberOfSegments)).isActive = true
UIView.animate(withDuration: 0.3) {
underlineBar.frame.origin.x = (self.segmentedControl.frame.width / CGFloat(self.segmentedControl.numberOfSegments)) * CGFloat(self.segmentedControl.selectedSegmentIndex)
}
}
}
The UIView.animate is being called, but nothing happens.

The problem is that you are setting constraints for underlineBar that are updating the X, Y and width, height properties so when you are trying to animate the change in position, the view is not moving because the constraints have higher priority. In conclusion you will need to animate the constraints.
I have created a global leftAnchor that you will modify every time the segment is pressed.
var underlineBarLeftAnchor: NSLayoutConstraint!
Where you setup your constraints update the function with this:
underlineBar.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor).isActive = true
underlineBar.heightAnchor.constraint(equalToConstant: 3).isActive = true
underlineBar.widthAnchor.constraint(equalTo: segmentedControl.widthAnchor, multiplier: 1 / CGFloat(segmentedControl.numberOfSegments)).isActive = true
underlineBarLeftAnchor = underlineBar.leftAnchor.constraint(equalTo: segmentedControl.leftAnchor, constant: 0)
underlineBarLeftAnchor.isActive = true
Now you will need to register the target-action methods for a segmented control using the valueChanged constant as shown below, update viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
setSegmentedControlStyle()
segmentedControl.addTarget(self, action: #selector(updateSegmentedControl), for: .valueChanged)
}
Last step, create the updateSegmentedControl() function where you will animate the movement:
This function will get called every time the value of the segment control is changed
#objc func updateSegmentedControl() {
let padding = underlineBar.frame.width * CGFloat(self.segmentedControl.selectedSegmentIndex)
underlineBarLeftAnchor.constant = padding
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
You need to calculate how much left padding you need and then update the ui, layoutIfNeeded(), while animating the change.

Related

is there a UIKit equivalent to SwiftUI's zstack?

I'm trying to create something like this. I've been working with SwiftUI recently so I know I could create that by adding an image, text and button (the I'm flexible text is the label for a button/NavigationLink) to a zstack. but I'm looking around trying to see if there's anyway to do that in UIKit. preferably without using storyboards. I'm open to a cocoapods library or whatever if that's what it takes. I've looked around and explored using SwiftUI to create the desired ZStack and then use it in my UIKit with a UIHostingController but because it involves a button/navigationlink. seeing as how the NavigationLink would require the destination to conform to a View, I wanted to ask around before converting even more of my project to swiftui. I was more hoping this project would be for giving me more experience building views in UIKit without storyboards so I'd prefer to do that instead of using SwiftUI. if that's possible I guess.
I've tried searching around but all my google searches involving UIButtons and images just link to posts about setting the image in a UIButton.
since you wanted to get more experience in creating views using UIKit, I've created a view that inherits from UIView that you can reuse. There's quite a lot of code to get the same result in UIKit. The code and output are provided below.
NOTE: Read the comments provided
Code
class ImageCardWithButton: UIView {
lazy var cardImage: UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false // To flag that we are using Constraints to set the layout
image.image = UIImage(named: "dog")
image.contentMode = .scaleAspectFill
return image
}()
lazy var gradientView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false // IMPORTANT IF YOU ARE USING CONSTRAINTS INSTEAD OF FRAMES
return view
}()
// VStack equivalent in UIKit
lazy var contentStack: UIStackView = {
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.distribution = .fillProportionally // Setting the distribution to fill based on the content
return stack
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.numberOfLines = 0 // Setting line number to 0 to allow sentence breaks
label.text = "Let your curiosity do the booking"
label.font = UIFont(name: "Raleway-Semibold", size: 20) // Custom font defined for the project
label.textColor = .white
return label
}()
lazy var cardButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .white
button.setTitle("I'm flexible", for: .normal)
button.setTitleColor(.blue, for: .normal)
// button.addTarget(self, action: #selector(someObjcMethod), for: .touchUpInside) <- Adding a touch event and function to invoke
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
self.addSubview(cardImage) // Adding the subview to the current view. i.e., self
// Setting the corner radius of the view
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
NSLayoutConstraint.activate([
cardImage.leadingAnchor.constraint(equalTo: self.leadingAnchor),
cardImage.trailingAnchor.constraint(equalTo: self.trailingAnchor),
cardImage.topAnchor.constraint(equalTo: self.topAnchor),
cardImage.bottomAnchor.constraint(equalTo: self.bottomAnchor),
])
setupGradientView()
addTextAndButton()
}
private func setupGradientView() {
let height = self.frame.height * 0.9 // Height of the translucent gradient view
self.addSubview(gradientView)
NSLayoutConstraint.activate([
gradientView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
gradientView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
gradientView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
gradientView.heightAnchor.constraint(equalToConstant: height)
])
// Adding the gradient
let colorTop = UIColor.clear
let colorBottom = UIColor.black
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [colorTop.cgColor, colorBottom.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.frame = CGRect(
x: 0,
y: self.frame.height - height,
width: self.frame.width,
height: height)
gradientView.layer.insertSublayer(gradientLayer, at:0)
print(self.frame)
}
private func addTextAndButton() {
// Adding the views to the stackview
contentStack.addArrangedSubview(titleLabel)
contentStack.addArrangedSubview(cardButton)
gradientView.addSubview(contentStack)
NSLayoutConstraint.activate([
contentStack.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20),
contentStack.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20), // Negative for leading and bottom constraints
contentStack.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20), // Negative for leading and bottom constraints
cardButton.heightAnchor.constraint(equalToConstant: 60)
])
cardButton.layer.cornerRadius = 30 // Half of the height of the button
}
}
Output
Important pointers
You can create the layout using constraints or frames. In case you are using constraints, it is important to set a views .translatesAutoresizingMaskIntoConstraints to false (You can read the documentation for it).
NSLayoutConstraint.activate([...]) Is used to apply an array of constraints at once. Alternatively, you can use:
cardImage.leadingAnchor.constraint(...)isActivated = true
for individual constraints
Manual layout of the views will sometimes require padding. So for this you will have to use negative or positive values for the padding based on the edge (side) of the view you are in. It's easy to remember to set the value of the padding in the direction of the centre of the view.
E.x., From the leading/left edge, you will need to add a padding of 10 towards the centre of the view or -10 from the right/trailing side towards the centre.

Using custom UIView class with Auto Layout returns incorrect bounds

I'm presenting a simple view controller. To follow MVC, I moved my programmatic view code into a separate UIView subclass. I followed the advice here of how to set up the custom view.
This works ok in iPhone. But, on iPad, the view controller is automatically presented with the new post-iOS 13 default modal style .pageSheet, which causes one of my buttons to be too wide. I looked into the view debugger and it's because the width constraint is set to self.bounds.width * 0.7, and self.bounds returns the full width of the iPad (1024 points) at the time the constraint is set, not the actual final view (which is only 704 points wide).
iPhone simulator screenshot
iPad 12.9" simulator screenshot
Three questions:
In general, am I setting up the custom view + view controller correctly? Or is there a best practice I'm not following?
How do I force an update to the constraints so that the view recognizes it's being presented in a .pageSheet modal mode on iPad, and automatically update the width of self.bounds? I tried self.view.setNeedsLayout() and self.view.layoutIfNeeded() but it didn't do anything.
Why is it that the Snooze button is laid out correctly on iPad, but not the Turn Off Alarm button... the Snooze button relies on constraints pinned to self.leadingAnchor and self.trailingAnchor. Why do those constraints correctly recognize the modal view they're being presented in, but self.bounds.width doesn't?
Code:
Xcode project posted here
View Controller using a custom view:
class CustomViewController: UIViewController {
var customView: CustomView {
return self.view as! CustomView
}
override func loadView() {
let customView = CustomView(frame: UIScreen.main.bounds)
self.view = customView // Set view to our custom view
}
override func viewDidLoad() {
super.viewDidLoad()
print("View's upon viewDidLoad is: \(self.view.bounds)") // <-- This gives incorrect bounds
// Add actions for buttons
customView.snoozeButton.addTarget(self, action: #selector(snoozeButtonPressed), for: .touchUpInside)
customView.dismissButton.addTarget(self, action: #selector(dismissButtonPressed), for: .touchUpInside)
}
override func viewDidAppear(_ animated: Bool) {
print("View's bounds upon viewDidAppear is: \(self.view.bounds)") // <-- This gives correct bounds
// This doesn't work
// //self.view.setNeedsLayout()
// // self.view.layoutIfNeeded()
}
#objc func snoozeButtonPressed() {
// Do something here
}
#objc func dismissButtonPressed() {
self.dismiss(animated: true, completion: nil)
}
}
Custom view code:
class CustomView: UIView {
public var snoozeButton: UIButton = {
let button = UIButton(type: .system)
button.isUserInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Snooze", for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitleColor(UIColor.white, for: .normal)
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.white.cgColor
button.layer.cornerRadius = 14
return button
}()
public var dismissButton: UIButton = {
let button = UIButton(type: .system)
button.isUserInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Turn off alarm", for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitleColor(UIColor.white, for: .normal)
button.backgroundColor = UIColor.clear
button.layer.cornerRadius = 14
button.layer.borderWidth = 2
button.layer.borderColor = UIColor.white.cgColor
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
self.backgroundColor = .systemPurple
// Add subviews
self.addSubview(snoozeButton)
self.addSubview(dismissButton)
}
func setupConstraints() {
print("Self.bounds when setting up custom view constraints is: \(self.bounds)") // <-- This gives incorrect bounds
NSLayoutConstraint.activate([
dismissButton.heightAnchor.constraint(equalToConstant: 60),
dismissButton.widthAnchor.constraint(equalToConstant: self.bounds.width * 0.7), // <-- This is the constraint that's wrong
dismissButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
dismissButton.centerYAnchor.constraint(equalTo: self.centerYAnchor),
snoozeButton.heightAnchor.constraint(equalToConstant: 60),
snoozeButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20),
snoozeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
snoozeButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -40)
])
}
}
.. and finally, the underlying view controller doing the presenting:
class BlankViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
let customVC = CustomViewController()
self.present(customVC, animated: true, completion: nil)
}
}
Thanks.
There's no need to reference bounds -- just use a relative width constraint.
Change this line:
dismissButton.widthAnchor.constraint(equalToConstant: self.bounds.width * 0.7), // <-- This gives incorrect bounds
to this:
dismissButton.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.7),
Now, the button will be 70% of the view's width. As an added benefit, it will adjust itself if/when the view width changes, instead of being stuck at a calculated Constant value.

UISegmentedControl Corner Radius Not Changing

UISegmentedControl corner radius is not changing. I also followed some answers in this question, my UISegmentedControl's corner radius still is not changing. I followed This tutorial to create UISegmentedControl.
Code:
import UIKit
class SegmentViewController: UIViewController {
private let items = ["Black", "Red", "Green"]
lazy var segmentedConrol: UISegmentedControl = {
let control = UISegmentedControl(items: items)
return control
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupViews()
}
fileprivate func setupViews(){
view.addSubview(segmentedConrol)
segmentedConrol.translatesAutoresizingMaskIntoConstraints = false //set this for Auto Layout to work!
segmentedConrol.heightAnchor.constraint(equalToConstant: 40).isActive = true
segmentedConrol.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 40).isActive = true
segmentedConrol.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -40).isActive = true
segmentedConrol.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
segmentedConrol.selectedSegmentIndex = 1
//style
segmentedConrol.layer.cornerRadius = 20
segmentedConrol.layer.borderWidth = 2
segmentedConrol.layer.borderColor = UIColor.black.cgColor
segmentedConrol.backgroundColor = .red
segmentedConrol.selectedSegmentTintColor = .darkGray
// segmentedConrol.clipsToBounds = true
segmentedConrol.layer.masksToBounds = true
}
}
(PS. Probably the answer is so simple for most people, please do not mind me, I am new in this field.)
Subclass UISegmentedControl and override layoutSubviews. Inside the method set the corner radius to what you want it to be, and you can remove the portion where you set the corner radius in setupViews():
class YourSegmentedControl: UISegmentedControl {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = 20
}
}
In your view controller where you create segmentedControl create an instance of YourSegmentedControl like below.
lazy var segmentedConrol: YourSegmentedControl = {
let control = YourSegmentedControl(items: items)
return control
}()
The result is:

Dynamic View in iOS inside UITableViewCell

I'm creating following view (display under In Transit) which are used to maintain my product status. See below image.
I want to create this view in UITableViewCell, I have tried by placing fixed height/width view (Circle View with different color) and horizontal gray line view and it's work fine for fixed spot point. I'm able to create this for fixed view using storyboard.
My Problem is, these are dynamic spot point view. Currently it's 4, but it can be vary based on status available in API response.
Anyone have idea? How to achieve this status spot dynamic view?.
You can achieve your thing using UICollectionView inside UITableViewCell.
First create following design for collection view cell. This collection view added inside table view cell.
CollectionViewCell:
See Constraints:
Regarding spotview and circleview you can recognise by constraints and view. So don't confuse therem otherwise all naming convention are available based on view's priority.
Now you need to take outlet of collection view inside UITableViewCell's subclass whatever you made and collection view cell's subview to UICollectionViewCell's subclass.
UITableViewCell:
class CTrackOrderInTransitTVC: UITableViewCell {
#IBOutlet weak var transitView : UIView!
#IBOutlet weak var cvTransit : UICollectionView!
var arrColors: [UIColor] = [.blue, .yellow, .green, .green]
override func awakeFromNib() {
super.awakeFromNib()
}
}
Now add following code in your collection view cell subclass, It's contains outlets of your subViews of collection view cell:
class CTrackOrderInTransitCVC: UICollectionViewCell {
#IBOutlet weak var leftView : UIView!
#IBOutlet weak var rightView : UIView!
#IBOutlet weak var spotView : UIView!
#IBOutlet weak var circleView : UIView!
}
Thereafter, you have to implemented table view datasource method load your collection view cell inside your table.
See the following code:
extension YourViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
//------------------------------------------------------------------------------
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CTrackOrderInTransitTVC", for: indexPath) as! CTrackOrderInTransitTVC
// Reload collection view to update sub views
cell.cvTransit.reloadData()
return cell
}
}
I hope this will help you.
You can do this with a UIStackView using "spacer" views.
Add a clear UIView between each "dot" view, and constrain the width of each "spacer" view equal to the first "spacer" view.
Add a UIStackView, constrain its width and centerY to the tracking line, and set the properties to:
Axis: Horizontal
Alignment: Fill
Distribution: Fill
Spacing: 0
Your code to add the "dots" will be something like this:
for i in 0..<numberOfDots {
create a dot view
add it to the stackView using .addArrangedSubview()
one fewer spacers than dots (e.g. 4 dots have a spacer between each = 3 spacers), so,
if this is NOT the last dot,
create a spacer view
add it to the stackView
}
Keep track of the spacer views, and set their width constraints each equal to the first spacer view.
Here is some starter code which may help you get going. The comments should make it clear what's being done. Everything is being done in code (no #IBOutlets) so you should be able to run it by adding a view controller in storyboard and assigning its custom class to DotsViewController. It adds the view as a "normal" subview... but of course can also be added as a subview of a cell.
class DotView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height * 0.5
}
}
class TrackingLineView: UIView {
var theTrackingLine: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return v
}()
var theStack: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fill
v.spacing = 0
return v
}()
var trackingDot: DotView = {
let v = DotView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(red: 0.0, green: 0.5, blue: 1.0, alpha: 1.0)
return v
}()
let dotWidth = CGFloat(6)
let trackingDotWidth = CGFloat(20)
var trackingDotCenterX = NSLayoutConstraint()
var dotViews = [DotView]()
var trackingPosition: Int = 0 {
didSet {
let theDot = dotViews[trackingPosition]
trackingDotCenterX.isActive = false
trackingDotCenterX = trackingDot.centerXAnchor.constraint(equalTo: theDot.centerXAnchor, constant: 0.0)
trackingDotCenterX.isActive = true
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add the tracking line
addSubview(theTrackingLine)
// add the "big" tracking dot
addSubview(trackingDot)
// add the stack view that will hold the small dots (and spacers)
addSubview(theStack)
// the "big" tracking dot will be positioned behind a small dot, so we need to
// keep a reference to its centerXAnchor constraint
trackingDotCenterX = trackingDot.centerXAnchor.constraint(equalTo: theTrackingLine.centerXAnchor, constant: 0.0)
NSLayoutConstraint.activate([
theTrackingLine.centerXAnchor.constraint(equalTo: centerXAnchor, constant: 0.0),
theTrackingLine.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0.0),
theTrackingLine.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0, constant: -20.0),
theTrackingLine.heightAnchor.constraint(equalToConstant: 2.0),
theStack.centerXAnchor.constraint(equalTo: theTrackingLine.centerXAnchor, constant: 0.0),
theStack.centerYAnchor.constraint(equalTo: theTrackingLine.centerYAnchor, constant: 0.0),
theStack.widthAnchor.constraint(equalTo: theTrackingLine.widthAnchor, multiplier: 1.0, constant: 0.0),
trackingDotCenterX,
trackingDot.widthAnchor.constraint(equalToConstant: trackingDotWidth),
trackingDot.heightAnchor.constraint(equalTo: trackingDot.widthAnchor, multiplier: 1.0),
trackingDot.centerYAnchor.constraint(equalTo: theTrackingLine.centerYAnchor, constant: 0.0),
])
}
func setDots(with colors: [UIColor]) -> Void {
// remove any previous dots and spacers
// (in case we're changing the number of dots after creating the view)
theStack.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
// reset the array of dot views
// (in case we're changing the number of dots after creating the view)
dotViews = [DotView]()
// we're going to set all spacer views to equal widths, so use
// this var to hold a reference to the first one we create
var firstSpacer: UIView?
colors.forEach {
c in
// create a DotView
let v = DotView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = c
// add to array so we can reference it later
dotViews.append(v)
// add it to the stack view
theStack.addArrangedSubview(v)
// dots are round (equal width to height)
NSLayoutConstraint.activate([
v.widthAnchor.constraint(equalToConstant: dotWidth),
v.heightAnchor.constraint(equalTo: v.widthAnchor, multiplier: 1.0),
])
// we use 1 fewer spacers than dots, so if this is not the last dot
if c != colors.last {
// create a spacer (clear view)
let s = UIView()
s.translatesAutoresizingMaskIntoConstraints = false
s.backgroundColor = .clear
// add it to the stack view
theStack.addArrangedSubview(s)
if firstSpacer == nil {
firstSpacer = s
} else {
// we know it's not nil, but we have to unwrap it anyway
if let fs = firstSpacer {
NSLayoutConstraint.activate([
s.widthAnchor.constraint(equalTo: fs.widthAnchor, multiplier: 1.0),
])
}
}
}
}
}
}
class DotsViewController: UIViewController {
var theButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.setTitle("Move Tracking Dot", for: .normal)
v.setTitleColor(.white, for: .normal)
return v
}()
var theTrackingLineView: TrackingLineView = {
let v = TrackingLineView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
return v
}()
var trackingDots: [UIColor] = [
.yellow,
.red,
.orange,
.green,
.purple,
]
var currentTrackingPosition = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 1.0, green: 0.8, blue: 0.5, alpha: 1.0)
view.addSubview(theTrackingLineView)
NSLayoutConstraint.activate([
theTrackingLineView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
theTrackingLineView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0),
theTrackingLineView.heightAnchor.constraint(equalToConstant: 100.0),
theTrackingLineView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
])
theTrackingLineView.setDots(with: trackingDots)
theTrackingLineView.trackingPosition = currentTrackingPosition
// add a button so we can move the tracking dot
view.addSubview(theButton)
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 40.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
theButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#objc func buttonTapped(_ sender: Any) -> Void {
// if we're at the last dot, reset to 0
if currentTrackingPosition < trackingDots.count - 1 {
currentTrackingPosition += 1
} else {
currentTrackingPosition = 0
}
theTrackingLineView.trackingPosition = currentTrackingPosition
UIView.animate(withDuration: 0.25, animations: {
self.view.layoutIfNeeded()
})
}
}
The result:
i recommend you use a collection view inside table cell, so that way you can define the position with a simple validation

Dynamically size UIStackView to the width of the UIViews inside

I am trying to create a UIStackView with three UIViews inside. The UIViews will have a circle with text over / in it.
I would like not to set the StackView to a static number, i would like it to be able to get smaller/grow based on the device the user is using.
Right now, the StackView is being added to the view, and the UIViews are being added to that. The colors are being displayed, but the rounded circles are not and the StackView height is not equal to the leftui's width.
Basically, I need three circles of equal height and width....is there a better way for this?
Here is my code.
#IBOutlet var stack: UIStackView!
override func viewWillLayoutSubviews() {
//let stack = UIStackView()
let leftui = UIView()
let middleui = UIView()
let rightui = UIView()
stack.addArrangedSubview(leftui)
stack.addArrangedSubview(middleui)
stack.addArrangedSubview(rightui)
leftui.backgroundColor = UIColor.red
middleui.backgroundColor = UIColor.blue
rightui.backgroundColor = UIColor.brown
leftui.bounds.size.height = leftui.bounds.width //needs these to new equal
middleui.bounds.size.height = middleui.bounds.width //needs these to new equal
rightui.bounds.size.height = rightui.bounds.width //needs these to new equal
leftui.layer.cornerRadius = leftui.bounds.size.width / 2
middleui.layer.cornerRadius = middleui.bounds.size.width / 2
rightui.layer.cornerRadius = rightui.bounds.size.width / 2
print(leftui.bounds.size.width) //prints 0.0
leftui.clipsToBounds = true
middleui.clipsToBounds = true
rightui.clipsToBounds = true
stack.sizeToFit()
stack.layoutIfNeeded()
view.addSubview(stack)
}
Here is what I was looking for.
This is from the android version of the application.
I think that in order for UIStackView to work its arrangedSubviews have to use autolayout - Check first answer here: Is it necessary to use autolayout to use stackview
This is how you could solve this:
Add a new class for your circular views, these do not do much other than set its layer.cornerRadius to half of their width, so that if height and width are the same they will be circular.
class CircularView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
clipsToBounds = true
layer.cornerRadius = bounds.midX
}
}
You add a widthConstraint with which you will be able to size the elements in the stack view
var widthConstraint: NSLayoutConstraint!
You can then create the UIStackView, I used your code mostly to do this:
override func viewDidLoad() {
super.viewDidLoad()
let leftui = CircularView()
let middleui = CircularView()
let rightui = CircularView()
leftui.translatesAutoresizingMaskIntoConstraints = false
middleui.translatesAutoresizingMaskIntoConstraints = false
rightui.translatesAutoresizingMaskIntoConstraints = false
leftui.backgroundColor = UIColor.red
middleui.backgroundColor = UIColor.blue
rightui.backgroundColor = UIColor.brown
let stack = UIStackView(arrangedSubviews: [leftui, middleui, rightui])
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
widthConstraint = leftui.widthAnchor.constraint(equalToConstant: 100)
NSLayoutConstraint.activate([
widthConstraint,
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
leftui.heightAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0),
middleui.widthAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0),
middleui.heightAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0),
rightui.widthAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0),
rightui.heightAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0)
])
}
Given the constraints set here, circles will have a width/height of 100 and stack view is centred in the view.
Next if you want to do something when view rotates you could implement something like this in your viewController
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { _ in
if size.width > size.height {
self.widthConstraint.constant = 150
} else {
self.widthConstraint.constant = 100
}
}, completion: nil)
}
It would animate to circles of width/height of 150 in landscape. You can then play with these values to get desired outcome.
To design this, follow the below steps.
create a custom view. in the custom view put all the subviews like
cost title label, price label and the color UIImageView
Now create three object of the custom view with proper data.
Get the device screen width divide by 3 gives each custom view
width, also set the view height as per your requirement and provide
frame for the created custom view
Now add the three views to the StackView.
Hope this will help to design, if you need any more help please comment.

Resources