Can you add a UIView to a UIStackview? - ios

I have an HStack of content like title, description, etc. Then I have some rectangles I want to add to the UIStackView programatically. But they do not appear.
In Playground in Swift 4
It works when appended to the overall view:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
// Add rects
let rectFrame: CGRect = CGRect(x:0, y:0, width:100, height:16)
let stepRect = UIView(frame: rectFrame)
stepRect.backgroundColor = UIColor.darkGray
let rectFrame2: CGRect = CGRect(x:50, y:16, width:100, height:16)
let stepRect2 = UIView(frame: rectFrame2)
stepRect2.backgroundColor = UIColor.red
self.view.addSubview(stepRect)
self.view.addSubview(stepRect2)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
This returns a white screen with two rectangles, one gray and one red, stacked on top of each other with some offset.
But is not working when appended to a UIStackView:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
// Make vstack
let vStack = UIStackView()
vStack.axis = .vertical
vStack.frame = CGRect(x: 0, y: 0, width: 300, height: 20)
// Add rects
let rectFrame: CGRect = CGRect(x:0, y:0, width:100, height:16)
let stepRect = UIView(frame: rectFrame)
stepRect.backgroundColor = UIColor.darkGray
let rectFrame2: CGRect = CGRect(x:50, y:16, width:100, height:16)
let stepRect2 = UIView(frame: rectFrame2)
stepRect2.backgroundColor = UIColor.red
vStack.addArrangedSubview(stepRect)
vStack.addArrangedSubview(stepRect2)
view.addSubview(vStack)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
This returns a white screen.

Yes, you can “add a UIView to a UIStackView.
But the whole point of a stack view is to let the placement of these “arranged” subviews. One supplies a distribution of the subviews (defaults to .fill) and an alignment of the subviews (again, defaults to .fill).
But you have specified ambiguous frames for the stack view and its subviews (which don’t happen to have any intrinsic size). Also, the notion of different x coordinates in a vertical stack view doesn't really make sense, either, as the alignment dictates the placement. If you run this in an actual app and then use the “view debugger”, it will report this ambiguity:
Unfortunately, playgrounds don't supply this sorts of diagnostic.
So, if using stack views, you either supply views with intrinsic sizes and let the stack view dictate the size and placement, or define supply the stack view subviews with size constraints and then ask it to manage the spacing between them (by using distribution of .equalSpacing or .equalCentering).
And if you really want to set the x and y values for your views manually, then it doesn't make sense to use a stack view at all.
A few examples: Perhaps you want define the two subviews to have the same size as each other, spaced 10 pt apart, for a total height of 50 pt:
let stackView = UIStackView(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .fill
stackView.spacing = 10
Or perhaps you want your two subviews of specific size spaced within this 50 pt tall stack view:
let stackView = UIStackView(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.alignment = .center
let grayView = UIView()
grayView.translatesAutoresizingMaskIntoConstraints = false
grayView.backgroundColor = .darkGray
let redView = UIView()
redView.translatesAutoresizingMaskIntoConstraints = false
redView.backgroundColor = .red
stackView.addArrangedSubview(grayView)
stackView.addArrangedSubview(redView)
view.addSubview(stackView)
NSLayoutConstraint.activate([
redView.widthAnchor.constraint(equalToConstant: 100),
redView.heightAnchor.constraint(equalToConstant: 16),
grayView.widthAnchor.constraint(equalToConstant: 100),
grayView.heightAnchor.constraint(equalToConstant: 16)
])
There are lots of other examples we could give you, but, in short, it simply doesn’t make sense to set frames of stack view and arranged subviews (esp with different x values) and hoping that the stack view will be able to reconcile this solely with with .fill distribution.

From Apple's UIStackView documentation:
For all distributions except the UIStackView.Distribution.fillEqually
distribution, the stack view uses each arranged view’s
intrinsicContentSize property when calculating its size along the
stack’s axis. UIStackView.Distribution.fillEqually resizes all the
arranged views so they are the same size, filling the stack view along
its axis. If possible, the stack view stretches all the arranged views
to match the view with the longest intrinsic size along the stack’s
axis.
Your UIViews have no content and so when the stack views lay them out it will not see any intrinsicContentSize it needs to preserve. To see a different behavior that shows how a stack view handles its arranged subviews, try putting non-empty text views or image views in the stack. You can also try setting
vStack.distribution = .fillEqually
If you want to be able to have a generic UIView hold the size you set, one way is to create your own minimal subclass of UIView and override its intrinsicContentSize property, returning the view's self.bounds.size

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.

Programmatically created shadow doesn't display

I have iOS app where I have a couple views with a shadow (Called them shadowView for convenience). This is how they're made:
let shadowView = UIView(frame: .zero)
self.addSubview(shadowView)
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0.5, height: 1.5)
shadowView.layer.shadowOpacity = 0.15
shadowView.layer.masksToBounds = false
shadowView.layer.cornerRadius = 8
shadowView.backgroundColor = .clear
shadowView.layer.shadowRadius = 5.0
shadowView.clipsToBounds = false
shadowView.layer.shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 20).cgPath
The view is then placed in position with Snapkit, but I left out that part of the code due to it being irrelevant. Now the shadow that I made here is not displayed. However, if I set the backgroundColor property to e.g. UIColor.yellow then the view itself shows, but not the shadow.
I also checked if the shadow might be cut off by any parent view, which doesn't seem to be the case.
As you can see it's not the usual clipsToBounds / masksToBounds mistake, and I've been looking at this for the last couple hours. Am I missing a piece of code maybe? Or did I miss anything?
Your frame size is .zero. Assign some valid size to it.
Give the view a non-zero frame size
let viewWidth = //any
let viewHeight = //any
let shadowView = UIView(frame: CGRect.init(x: 0, y: 0, width: viewWidth, height: viewHeight))
To try and clarify...
You are setting .shadowPath, but based on you code you're setting it prior to .shadowView having its frame set. So, you are creating a UIBezierPath with a frame of .zero -- and it never changes.
You need to set the shadow path either in viewDidLayoutSubviews of a view controller, or by overriding layoutSubviews() inside your custom view.
Here is a simple example that you can run in a Playground page:
import UIKit
import PlaygroundSupport
class TestViewController : UIViewController {
let shadowView = UIView(frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(shadowView)
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOpacity = 0.15
shadowView.layer.cornerRadius = 8
shadowView.backgroundColor = .clear
// this would be handled by your use of SnapKit
shadowView.translatesAutoresizingMaskIntoConstraints = false
shadowView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40.0).isActive = true
shadowView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
shadowView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
shadowView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
shadowView.layer.shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 20).cgPath
}
}
let vc = TestViewController()
vc.view.backgroundColor = .white
PlaygroundPage.current.liveView = vc
Like other colleagues said, the frame of your view is the problem.
This is a problem just because you set the layer's shadow path to be as big as the view's bounds that in that moment is .zero.
Still I understand that you want your view to react to its constraint when the auto-layout engine is ready to layout your views. There are then two things you can do to address this problem:
1) you can remove the explicit set of shadowView.layer.shadowPath and change the color of the view from clear to another color. This will cause the shadow to follow the shape of the view even if it changes, with a dynamic shadow path. While this will fix the issue, sometimes it results also in an impact on performance during animations and transitions.
2) if the view doesn't change its size during her life on screen you can force the superView to re-layout itself right after you add the shadowView as subview.
self.addSubview(shadowView)
self.setNeedsLayout()
self.layoutIfNeeded()
In this way when you'll create the shadow path bounds will not be zero because auto layout already did its magic changing the frame of your view.
Please note that with the second solution you will not see the view (that has clear color) but just its shadow.
Swift 4
Add this function in your view controller
func addShadow(myView: UIView?) {
myView?.layer.shadowColor = UIColor.black.cgColor
myView?.layer.shadowOffset = CGSize(width: 0, height:0)
myView?.layer.shadowOpacity = 0.3
myView?.layer.shadowRadius = 5
myView?.layer.masksToBounds = false
myView?.layer.cornerRadius = 12
}
and use it like:
let shadowView = UIView(frame: CGRect(x: 0, y: 0, width: yourWidth, height: yourHeight))
self.addShadow(shadowView)

Autolayout for dynamically added UIViews

I need some recommendation on how to add constraints to dynamically added UILabels
Say I have a UIView called (A) of size (screenwidth, 200) added to a UIViewcontroller view. On view A, I have set the autoresize masks on it's leading edge and trailing edge with the superview.
Now at runtime, I am adding UILabels with characters from a String. For example, if the string is "HELLO", I am adding five UILabels at equally spaced intervals for each character in HELLO.
When the orientation changes from portrait to landscape the UILabels are not evenly spaced. I haven't set any constraints on the UILabels and need some suggestion on how to add the constraints.
Is there any other solution other than constraints available to make the UILabels evenly spread out in landscape and portrait mode?
Thanks
I can recommend using UIStackViews. Sweeper's solution is not using dynamic addition to the screen, so I thought I would provide a programmatic solution.
To add these dynamically, use the following example.
First:
#IBOutlet weak var containerView: UIView!
This should be view A from your description, and make sure to hook this up with the storyboard.
To add labels, we first add a stack view to the container view:
let stackView = UIStackView()
stackView.distribution = .fillEqually
stackView.alignment = .fill
stackView.spacing = 15
stackView.autoresizingMask = [.flexibleLeftMargin,.flexibleRightMargin]
containerView.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: (stackView.superview?.centerXAnchor)!, constant: 0).isActive = true
stackView.centerYAnchor.constraint(equalTo: (stackView.superview?.centerYAnchor)!, constant: 0).isActive = true
stackView.heightAnchor.constraint(equalTo: (stackView.superview?.heightAnchor)!, constant: 0).isActive = true
stackView.widthAnchor.constraint(equalToConstant: 200).isActive = true
You can play around with the width anchor to get what you want.
Then when you want to add a label, use this:
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 5, height: 5))
label.text = "E"
stackView.addArrangedSubview(label)
let labelTwo = UILabel(frame: CGRect(x: 0, y: 0, width: 5, height: 5))
labelTwo.text = "L"
stackView.addArrangedSubview(labelTwo)
//... And so on
Stack views make it easy to add subviews without too many constraints. However, you do have to add constraints to the stack view itself to get something on the screen!
I hope this helps.
In iOS 9, UIStackViews got introduced. This is a great opportunity to use them!
The stack view will have constraints on leading, trailing and other necessary properties so that it stays where you want. The distribution of the stack view should be "Fill Equally".
Add an outlet to your VC and when needed, add your labels into the stack view!

Center label in UIView which has y offset of height of status bar

I am quite new to swift and trying to center a label and an image in a UIView which is located at the top of the screen. Currently the label is centered vertically and horizontally since this is the only thing I am able to do right now. As you can see I set autoresizing mask into constraints to false and used centerXAnchor and -YAnchor.
However I actually do not want the label to be in the center of the PostView but rather centered with a y offset of the height of the status bar. So it is centered but with no y offset of the height of the statusbar. Consequently, it looks kind of cramped(?): It is very close to the status bar... It looks like this:
But I would like to have the label (and later also an image) vertically centered in the red box:
This is the code I have right now (PostView class):
override init(frame: CGRect){
super.init(frame: frame)
//add subview containing name (and image)
infosContainerView.frame = frame
addSubview(infosContainerView)
//add sub view containing label to former UIView (infosContainerView)
infosContainerView.addSubview(infoNameView)
infoNameView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
infoNameView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
//this UIView shall contain the infoNameView and infoImageView
let infosContainerView: UIView = {
//set properties of controls container view
let entireInfoView = UIView()
entireInfoView.backgroundColor = .white
return entireInfoView
}()
//label and properties of label with name (autoresizingmaskinto constraint set to false)
let infoNameView: UILabel = {
//set properties of controls container view
let nameView = UILabel()
nameView.translatesAutoresizingMaskIntoConstraints = false
nameView.backgroundColor = .white
nameView.font = UIFont(name: "HelveticaNeue", size: 20)
nameView.text = "Name"
nameView.textColor = .black
nameView.textAlignment = .center
return nameView
}()
EDIT:
Jože Ws was close to solving the problem, instead of dividing by 2 one has to divide by 4 although I do not know why...:
let statusBarHeight = UIApplication.shared.statusBarFrame.height
infosContainerView.addSubview(infoNameView)
infoNameView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
infoNameView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: statusBarHeight/4).isActive = true
Screenshot:
Replace centerYAnchor constraint init with
// infoNameView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
let statusBarHeight = UIApplication.shared.statusBarFrame.height
infoNameView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: statusBarHeight/2).isActive = true
This will add an offset to centerYAnchor equal to the value of the statusBarHeight

centering titleView in navigationItem

I am trying to center my titleView in my navigationItem.
After localizing my app, I saw this behavior:
This is my code as an extension to navigationItem:
extension UINavigationItem{
func makeImg(){
let container = UIView(frame: CGRect(x: 0,y: 0,width: 200,height: 40))
let logo = UIImage(named: "Rookie")
let imageView = UIImageView(frame: CGRect(x: 66.75, y: 7.25, width: 66.5, height: 25.5))
imageView.image = logo
imageView.layer.masksToBounds = true
imageView.clipsToBounds = true
container.addSubview(imageView)
self.titleView = container
}
}
I think it has something to do with my superView in navigationItem.
So I should reference the Screens bounds. Any ideas?
This happens if the view set as titleView is very wide. Views that are added as a titleView are only resized if their width is wider than the available space, in which case they're resized to fill the space between the left bar button item and the right.
In this case, you've set your container object as being 200 points wide, which is wider than the available space with both of those items.
If you set the container width to match the image view (i.e., 66.5 points), then it should become centered. :)
(Or conversely, if you don't plan to add any other views, you can set the image view directly as the titleView)

Resources