I have a custom view controller used as a child view controller:
class ChildViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
calculatePreferredSize()
}
func calculatePreferredSize() {
let targetSize = CGSize(width: view.bounds.width,
height: UIView.layoutFittingCompressedSize.height)
preferredContentSize = view.systemLayoutSizeFitting(targetSize)
}
}
then in the main view controller, I have this code:
class ViewController: UIViewController {
var container : UIView!
var childVC : ChildViewController!
var containerHeightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .purple
// setup container to hold child vc
container = UIView()
container.backgroundColor = .systemPink
container.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(container)
container.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
container.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
container.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
containerHeightConstraint = NSLayoutConstraint()
containerHeightConstraint = container.heightAnchor.constraint(equalToConstant: 0)
containerHeightConstraint.isActive = true
// setup child vc
childVC = ChildViewController()
addChild(childVC)
container.addSubview(childVC.view)
childVC.view.frame = container.bounds
childVC.didMove(toParent: self)
// add contents into the child vc
let newView = UIView()
childVC.view.addSubview(newView)
newView.backgroundColor = .systemBlue
newView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
newView.topAnchor.constraint(equalTo: newView.superview!.topAnchor),
newView.leadingAnchor.constraint(equalTo: newView.superview!.leadingAnchor),
newView.trailingAnchor.constraint(equalTo: newView.superview!.trailingAnchor),
newView.heightAnchor.constraint(equalToConstant: 123),
])
}
override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) {
super.preferredContentSizeDidChange(forChildContentContainer: container)
if (container as? ChildViewController) != nil {
containerHeightConstraint.constant = container.preferredContentSize.height
}
}
}
I am trying to dynamically size the container view in the main VC based on the child's calculated height. The preferredContentSizeDidChange method is being called but in my calculation of the child VC's height (using UIView.layoutFittingCompressedSize), I'm always getting back 0. Even though I've checked the frame of the view added onto that view and it has the correct frame height (in this example, 123). As shown in the output logging below:
(lldb) po view.subviews
▿ 1 element
- 0 : <UIView: 0x12251cd40; frame = (0 0; 350 123); layer = <CALayer: 0x6000007a0e60>>
(lldb) po UIView.layoutFittingCompressedSize
▿ (0.0, 0.0)
- width : 0.0
- height : 0.0
Below is a screenshot from the simulator.
Am I using the UIView.layoutFittingCompressedSize incorrectly? How do I calculate the height of the child view based on its contents?
Autolayout can't calculate the newView content height, because it is missing constraints in the Y axis to solve the equation.
newView has only these constraints defined: top, leading, trailing and height.
It is missing the bottom constraint:
newView.bottomAnchor.constraint(equalTo: newView.superview!.bottomAnchor).isActive = true
The full set of constraints would look like the following:
NSLayoutConstraint.activate([
newView.topAnchor.constraint(equalTo: newView.superview!.topAnchor),
newView.leadingAnchor.constraint(equalTo: newView.superview!.leadingAnchor),
newView.trailingAnchor.constraint(equalTo: newView.superview!.trailingAnchor),
newView.heightAnchor.constraint(equalToConstant: 123),
newView.bottomAnchor.constraint(equalTo: newView.superview!.bottomAnchor)
])
Afterwards when I place a breakpoint into preferredContentSizeDidChange, I can print the container.preferredContentSize.height, which is 123.0.
EDIT
To avoid constraint breakage, we also need to use autolayout for childVC.view. Right now it is using autosizing mask, which only flows from top-down and creates constraints with 1000 priority.
childVC.view.frame = container.bounds
needs to be replaced with
childVC.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
childVC.view.topAnchor.constraint(equalTo: container.topAnchor),
childVC.view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
childVC.view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
childVC.view.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
and the containerHeightConstraint needs to have a reduced priority for the 0 height constraint, otherwise the system will always find the constraints ambiguous - the child controller wants to be 123 points tall, but the container height constraint is still at 0 before we call the preferredContentSizeDidChange method.
containerHeightConstraint.priority = .defaultLow
Related
-This was my Question: This is an audio player, I removed all the other lines for you to be easy in reading. The problem is in MPVolumeView. When user swipes all the way to maximum the button of the slider hovers over the connectivity button. When user swipes the button of the slider all the way to minimum the button of the slider doesn't move to the end.
-Dear DonMag, I am really thankful to you, It works! and HOW! I am adding screenshots. I believe your answer will be helpful to a lot of self tights.
import UIKit
import AVFoundation
import MediaPlayer
import AVKit
class AudioPlayerViewControllerQ1: UIViewController {
#IBOutlet var holder: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if holder.subviews.count == 0 {
}
let volumeView = MPVolumeView(frame: CGRect(x: 20,
y: holder.frame.size.height - 80,
width: holder.frame.size.width-40,
height: 30))
holder.addSubview(volumeView)
}
private func setupView() {
setupConstraints()
}
private func setupConstraints() {
NSLayoutConstraint.activate([
holder.leadingAnchor.constraint(equalTo: view.leadingAnchor),
holder.trailingAnchor.constraint(equalTo: view.trailingAnchor),
holder.topAnchor.constraint(equalTo: view.topAnchor),
holder.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIApplication.shared.isIdleTimerDisabled = true
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
UIApplication.shared.isIdleTimerDisabled = false
}
}
After quick research and experimentation -- it appears MPVolumeView is rather buggy :(
When instantiated, if the current device volume is greater than 0, the thumb will be offset on the x-axis. The higher the volume, the larger the offset.
Also, it doesn't play well at all with auto-layout constraints.
We can get around this by subclassing MPVolumeView and "fixing" the slider rect:
class MyVolumeView: MPVolumeView {
override func volumeSliderRect(forBounds bounds: CGRect) -> CGRect {
// this will avoid the thumb x-offset issue
// while keeping the route button vertically aligned
return bounds.insetBy(dx: 12.0, dy: 0.0).offsetBy(dx: -12.0, dy: -5.0)
}
}
Then, to correct the problems with the vertical layout, we will want to offset the Y position when we set its frame.
Here's a quick example of one way to do that. I've embedded MyVolumeView in a "container" view, and used a property observer to update the frame whenever the container view's bounds changes:
class AudioPlayerViewControllerQ1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// let's give the view a background color so we can easily see its frame
view.backgroundColor = .systemYellow
// assuming "holder" view has buttons and other controls
// along with the MyVolumeView near the bottom
let holder = UIView()
holder.backgroundColor = .darkGray
holder.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(holder)
// create a separate "container" view for the MyVolumeView
let volumeViewContainer = UIView()
// we'll make it red for now so we can see it
volumeViewContainer.backgroundColor = .red
volumeViewContainer.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(volumeViewContainer)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's make the holder 20-points inset on leading/trailing
holder.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
holder.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// holder height (for this example) is 240.0
holder.heightAnchor.constraint(equalToConstant: 240.0),
// let's put its bottom 60-points from the bottom (of the safe area)
holder.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -160.0),
// volume view container leading/trailing equal to holder
volumeViewContainer.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 0.0),
volumeViewContainer.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: 0.0),
// volume view container bottom equal to holder bottom
volumeViewContainer.bottomAnchor.constraint(equalTo: holder.bottomAnchor, constant: 0.0),
// volume view container height equal to 30-points
volumeViewContainer.heightAnchor.constraint(equalToConstant: 30.0),
])
// now we'll add a MPVolumeView to the container
let v = MyVolumeView()
volumeViewContainer.addSubview(v)
// we'll use a property observer to update the MyVolumeView frame
// whenever the container bounds changes
volumeViewContainer.addObserver(self, forKeyPath: "bounds", context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "bounds" {
// make sure we're getting notified of the MyVolumeView container view
if let cv = object as? UIView,
let mpv = cv.subviews.first as? MyVolumeView {
// set MyVolumeView frame to container view's bounds
// and offset its y-position by 4-points (because of its buggy layout)
mpv.frame = cv.bounds.offsetBy(dx: 0.0, dy: 4.0)
}
}
}
}
It looks like this when running:
and we can drag the thumb all the way to the left:
and to the right (without overlapping the route button):
Edit
Here are a couple simplified examples...
Using CGRect frames instead of constraints (as requested by the OP):
class AudioPlayerViewControllerQ1: UIViewController {
let holder = UIView()
let myVolumeView = MyVolumeView()
override func viewDidLoad() {
super.viewDidLoad()
// let's give the view a background color so we can easily see its frame
view.backgroundColor = .systemYellow
// assuming "holder" view has buttons and other controls
// along with the MPVolumeView near the bottom
holder.backgroundColor = .darkGray
view.addSubview(holder)
// now we'll add a MPVolumeView to the container
holder.addSubview(myVolumeView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// let's make sure this only executes if the holder view frame has not been set yet
if holder.frame.width != 320.0 {
// set holder view frame to 320 x 240
holder.frame = CGRect(x: 0, y: 0, width: 320.0, height: 240.0)
// center it in the view
holder.center = view.center
// set myVolumeView frame to same width as holder view
// 30-points height, at bottom of holder view
myVolumeView.frame = CGRect(x: 0.0, y: holder.frame.height - 30.0, width: holder.frame.width, height: 30.0)
}
}
}
and, this one using constraints:
class AudioPlayerViewControllerQ1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// let's give the view a background color so we can easily see its frame
view.backgroundColor = .systemYellow
// assuming "holder" view has buttons and other controls
// along with the MPVolumeView near the bottom
let holder = UIView()
holder.backgroundColor = .darkGray
holder.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(holder)
// now we'll add a MPVolumeView to the container
let myVolumeView = MyVolumeView()
myVolumeView.translatesAutoresizingMaskIntoConstraints = false
holder.addSubview(myVolumeView)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// set holder view frame to 320 x 240
holder.widthAnchor.constraint(equalToConstant: 320.0),
holder.heightAnchor.constraint(equalToConstant: 240.0),
// center it
holder.centerXAnchor.constraint(equalTo: g.centerXAnchor),
holder.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain myVolumeView leading/trailing/bottom equal to holder view
myVolumeView.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 0.0),
myVolumeView.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: 0.0),
myVolumeView.bottomAnchor.constraint(equalTo: holder.bottomAnchor, constant: 0.0),
// myVolumeView height
myVolumeView.heightAnchor.constraint(equalToConstant: 30.0),
])
}
}
I had the following code in Swift to fill a status bar within its container, in relation to the completion of a quiz percentage by changing its width dynamically and it worked fine in 2018:
func updateUI() {
questionCounter.text = "\(Texts.questionCounter) \(questionNumber + 1)"
progressBar.frame.size.width = (containerOfBar.frame.size.width / CGFloat(allQuestions.list.count)) * CGFloat(questionNumber)
}
The instantiation of the elements have been made by closures in this way:
private let containerOfBar: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.layer.cornerRadius = 8
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 2
return view
}()
private let progressBar: UIView = {
let bar = UIView()
bar.backgroundColor = .blue
bar.translatesAutoresizingMaskIntoConstraints = false
return bar
}()
The auto-layout graphic constraints for the container and the bar, have been set in the following code only without a storyboard.
The bar itself:
progressBar.leadingAnchor.constraint(equalTo: containerOfBar.leadingAnchor, constant: 2),
progressBar.topAnchor.constraint(equalTo: containerOfBar.topAnchor, constant: 2),
progressBar.bottomAnchor.constraint(equalTo: containerOfBar.bottomAnchor, constant: 2),
The container of the bar:
containerOfBar.centerXAnchor.constraint(equalTo: optionsViewContainer.centerXAnchor),
containerOfBar.topAnchor.constraint(equalTo: optionsView[enter image description here][1].bottomAnchor, constant: self.view.frame.size.height/42),
containerOfBar.bottomAnchor.constraint(equalTo: optionsViewContainer.bottomAnchor, constant: -self.view.frame.size.height/42),
containerOfBar.widthAnchor.constraint(equalTo: optionsViewContainer.widthAnchor, multiplier: 0.3),
In the link, there is the image of the completion bar drawn by code.
Can't understand why the frame.width property doesn't work anymore, maybe a change in constraints workflow logic that I am missing...
I tried also to use the code of the function separately, but it seems like frame.width is not dynamically usable anymore.
Any suggestions?
You are mixing constraints with explicit frame settings, which won't give you the desired results. Each time auto-layout updates the screen, it will reset your progressBar.frame.size.width back to its constraint value -- in this case, it will be Zero because you didn't give it one.
A better approach is to set a Width Anchor on the progressBar. Make it equal to the Width Anchor of containerOfBar, with a multiplier of the percent of progress, and a constant of -4 (so you have 2-pts on each side).
Here's an example. It uses a questionCounter of 10 ... each time you tap the screen, it will increment the "current question number" and update the progress bar:
class ProgViewController: UIViewController {
private let containerOfBar: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.layer.cornerRadius = 8
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 2
return view
}()
private let progressBar: UIView = {
let bar = UIView()
bar.backgroundColor = .blue
bar.translatesAutoresizingMaskIntoConstraints = false
return bar
}()
private let questionCounter: UILabel = {
let v = UILabel()
v.backgroundColor = .cyan
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var numberOfQuestions = 10
var questionNumber = 0
// width constraint of progressBar
var progressBarWidthConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
containerOfBar.addSubview(progressBar)
view.addSubview(containerOfBar)
view.addSubview(questionCounter)
// create width constraint of progressBar
// start at 0% (multiplier: 0)
// this will be changed by updateUI()
progressBarWidthConstraint = progressBar.widthAnchor.constraint(equalTo: containerOfBar.widthAnchor, multiplier: 0, constant: -4)
progressBarWidthConstraint.priority = .defaultHigh
NSLayoutConstraint.activate([
progressBarWidthConstraint,
progressBar.leadingAnchor.constraint(equalTo: containerOfBar.leadingAnchor, constant: 2),
progressBar.topAnchor.constraint(equalTo: containerOfBar.topAnchor, constant: 2),
progressBar.bottomAnchor.constraint(equalTo: containerOfBar.bottomAnchor, constant: -2),
//The container of the bar:
containerOfBar.centerXAnchor.constraint(equalTo: view.centerXAnchor),
containerOfBar.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
containerOfBar.heightAnchor.constraint(equalToConstant: 50),
containerOfBar.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
// label under the container
questionCounter.topAnchor.constraint(equalTo: containerOfBar.bottomAnchor, constant: 8.0),
questionCounter.leadingAnchor.constraint(equalTo: containerOfBar.leadingAnchor),
questionCounter.trailingAnchor.constraint(equalTo: containerOfBar.trailingAnchor),
])
// every time we tap on the screen, we'll increment the question number
let tap = UITapGestureRecognizer(target: self, action: #selector(self.nextQuestion(_:)))
view.addGestureRecognizer(tap)
updateUI()
}
#objc func nextQuestion(_ g: UITapGestureRecognizer) -> Void {
// increment the question number
questionNumber += 1
// don't exceed number of questions
questionNumber = min(numberOfQuestions - 1, questionNumber)
updateUI()
}
func updateUI() {
questionCounter.text = "Question: \(questionNumber + 1) of \(numberOfQuestions) total questions."
// get percent completion
// for example, if we're on question 4 of 10,
// percent will be 0.4
let percent: CGFloat = CGFloat(questionNumber + 1) / CGFloat(numberOfQuestions)
// we can't change the multiplier directly, so
// deactivate the width constraint
progressBarWidthConstraint.isActive = false
// re-create it with current percentage of width
progressBarWidthConstraint = progressBar.widthAnchor.constraint(equalTo: containerOfBar.widthAnchor, multiplier: percent, constant: -4)
// activate it
progressBarWidthConstraint.isActive = true
// don't mix frame settings with auto-layout constraints
//progressBar.frame.size.width = (containerOfBar.frame.size.width / CGFloat(allQuestions.list.count)) * CGFloat(questionNumber)
}
}
It will look like this:
I’m trying to rip a view from a stackView that is embedded in a scrollView and then reposition said view in the same location but in another view at the same level in the view hierarchy as the scrollView.
The effect I’m trying to achieve is that I’m animating the removal of a view— where the view would be super imposed in another view, while the scrollView would scroll up and new view would be added to the stackView all while the view that was ripped fades out.
Unfortunately, achieving this effect remains elusive as the rippedView is position at (x: 0, y: 0). When I try force a new frame onto this view its tough because Im guessing the pixel perfect correct frame. Here’s a bit of the code from my viewController:
/*
I tried to make insertionView and imposeView have the same dimensions as the scrollView and
the stackView respectively as I thought if the rippedView’s original superView is the same
dimensions as it’s new superView, the rippedView would be positioned in the same place
without me needing to alter its frame.
*/
let insertionView = UIView(frame: scrollView.frame)
let imposeView = UIView(frame: stackView.frame)
rippedView.removeFromSuperview()
insertionView.addSubview(imposeView)
imposeView.addSubview(rippedView)
let newFrame = CGRect(x: 0, y: 450, width: rippedView.intrinsicContentSize.width, height:
rippedView.intrinsicContentSize.height)
rippedView.frame = newFrame
self.view.addSubview(insertionView)
Before removing rippedView, get it's actual frame:
let newFrame = self.view.convert(rippedView.bounds, from: rippedView)
The issue you are hitting is likely due to the stackView's arranged subviews having .translatesAutoresizingMaskIntoConstraints set to false. I believe this happens automatically when you add a view to a stackView, unless you specify otherwise.
A stackView's arranged subviews have coordinates relative to the stackView itself. So the first view will be at 0,0. Since you are adding a "container" view with the same frame as the stackView, you can use the same coordinate space... but you'll need to enable .translatesAutoresizingMaskIntoConstraints.
Try it like this:
#objc func btnTapped(_ sender: Any?) -> Void {
// get a reference to the 3rd arranged subview in the stack view
let rippedView = stackView.arrangedSubviews[2]
// local var holding the rippedView frame (as set by the stackView)
// get it before moving view from stackView
let r = rippedView.frame
// instantiate views
let insertionView = UIView(frame: scrollView.frame)
let imposeView = UIView(frame: stackView.frame)
// add imposeView to insertionView
insertionView.addSubview(imposeView)
// add insertionView to self.view
self.view.addSubview(insertionView)
// move rippedView from stackView to imposeView
imposeView.addSubview(rippedView)
// just to make it easy to see...
rippedView.backgroundColor = .green
// set to TRUE
rippedView.translatesAutoresizingMaskIntoConstraints = true
// set the frame
rippedView.frame = r
}
Here's a full class example that you can run directly (just assign it to a view controller):
class RipViewViewController: UIViewController {
let aButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.setTitle("Testing", for: .normal)
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemBlue
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 8
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(aButton)
view.addSubview(scrollView)
scrollView.addSubview(stackView)
let g = view.safeAreaLayoutGuide
let sg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
aButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 16.0),
aButton.centerXAnchor.constraint(equalTo: g.centerXAnchor, constant: 0.0),
scrollView.topAnchor.constraint(equalTo: aButton.bottomAnchor, constant: 40.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
stackView.topAnchor.constraint(equalTo: sg.topAnchor, constant: 40.0),
stackView.leadingAnchor.constraint(equalTo: sg.leadingAnchor, constant: 20.0),
stackView.trailingAnchor.constraint(equalTo: sg.trailingAnchor, constant: 20.0),
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -40.0),
stackView.bottomAnchor.constraint(equalTo: sg.bottomAnchor, constant: 20.0),
])
for i in 1...5 {
let l = UILabel()
l.backgroundColor = .cyan
l.textAlignment = .center
l.text = "Label \(i)"
stackView.addArrangedSubview(l)
}
aButton.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
}
#objc func btnTapped(_ sender: Any?) -> Void {
// get a reference to the 3rd arranged subview in the stack view
let rippedView = stackView.arrangedSubviews[2]
// local var holding the rippedView frame (as set by the stackView)
// get it before moving view from stackView
let r = rippedView.frame
// instantiate views
let insertionView = UIView(frame: scrollView.frame)
let imposeView = UIView(frame: stackView.frame)
// add imposeView to insertionView
insertionView.addSubview(imposeView)
// add insertionView to self.view
self.view.addSubview(insertionView)
// move rippedView from stackView to imposeView
imposeView.addSubview(rippedView)
// just to make it easy to see...
rippedView.backgroundColor = .green
// set to TRUE
rippedView.translatesAutoresizingMaskIntoConstraints = true
// set the frame
rippedView.frame = r
}
}
In my MainVC, I'm trying to constrain an UIView to the top, left, right and have a height of 80. Right now, my view is full screen. How would I fix my code to be able to have the right size?
// Variables
var topViewCons : [NSLayoutConstraint] = []
// Constants
let topGradient = RadialGradientLayer()
let topMainView = UIView()
// MainVC Top View Constraints
topMainView.translatesAutoresizingMaskIntoConstraints = false
topGradient.frame = view.bounds
topMainView.layer.addSublayer(topGradient)
self.view.addSubview(topMainView)
let topConstraint = topMainView.topAnchor.constraint(equalTo: self.view.topAnchor)
let leftConstraint = topMainView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor)
let rightConstraint = topMainView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
let topViewHeight = topMainView.heightAnchor.constraint(equalToConstant: 80)
NSLayoutConstraint.activate([topConstraint, leftConstraint, rightConstraint, topViewHeight])
It's not full screen , the gradient is
topGradient.frame = view.bounds // here you make it's frame to screen bounds
topMainView.layer.addSublayer(topGradient)
so you need to set
topMainView.clipsToBounds = true
OR
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
topGradient.frame = topMainView.bounds
}
Also you can do this directly without lets
NSLayoutConstraint.activate([
topMainView.topAnchor.constraint(equalTo: self.view.topAnchor),
topMainView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
topMainView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
topMainView.heightAnchor.constraint(equalToConstant: 80)
])
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.