Search code examples
iosswiftuiuikitswift5zstack

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.


Solution

  • 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

    enter image description here

    Important pointers

    1. 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).

    2. NSLayoutConstraint.activate([...]) Is used to apply an array of constraints at once. Alternatively, you can use:

    cardImage.leadingAnchor.constraint(...)isActivated = true
    

    for individual constraints

    1. 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.