Monday, January 02, 2023

UILabel UITextField UIStackView Horizontal

Introduction


This is "one" approach of using a horizontal StackView to have a label next to a text input field that keeps the space between the label and the text field during device rotation and on different screen sizes.

This approach does not use custom values for "Content Hugging Priority" or "Content Compression Resistance Priority".

There is one custom class that is needed. I called it UIViewExtended. You can call it anything you like.


//

//  UIViewExtended.swift

//  UITests

//

//  Created by Geoffrey Slinker on 12/24/22.

//


import Foundation

import UIKit


class UIViewExtended : UIView {

    

    enum Keys: String {

      case proportionalSize = "ProportionalSize"

    }

    

    var proportionalSize : CGSize

    

    

    override init(frame: CGRect) {

        proportionalSize = CGSize(width:1.0, height:1.0)

        super.init(frame:frame)

    }

    

    required init?(coder aDecoder: NSCoder) {

        let tempSize : CGSize? = aDecoder.decodeCGSize(forKey: Keys.proportionalSize.rawValue)

        if tempSize != nil  && ( tempSize?.width != 0 && tempSize?.height != 0 ) {

            proportionalSize = tempSize!

        }

        else {

            proportionalSize = CGSize(width:1.0, height:1.0)

        }

        super.init(coder: aDecoder)

    }

    

    override func encode(with coder: NSCoder) {

        coder.encode(self.proportionalSize, forKey: Keys.proportionalSize.rawValue)

        super.encode(with: coder)

    }

    

    @IBInspectable var intrinsicSize: CGSize {

        get {

            return proportionalSize

        }

        set {

            proportionalSize = newValue

        }

    }

    

    override open var intrinsicContentSize : CGSize {

        get {

            return proportionalSize

        }

    }

    

}




The approach is to use UIViews to contain the label and text views. I call this the nested view approach.

Here is the entire layout.




Steps

I

Using XCode create an iOS app with a single view. Add a ContainerView. Inside the Container View (image above - center panel - the Container View is on the right). Inside the Container View add a UIStackView Vertical.


Configure the UIStackView's alignment to be "fill" and distribution to be "fill".



Configure the UIStackView to fill the Container View. Set the constraints as shown above.

II

Add three UIViews to the UIStackView. The top two views will hold other views and the bottom view is used to control spacing.


Take notice of the constraints for these three views. The top two views have a height constraint of 40.

I set the background color of each view to something different so that I can see their locations and boundaries more easily.

III

In the top two views add to each a UIStackView that is horizontal.


Set alignment to "Fill" and distribution to "Fill Proportionally".


Constrain the UIStackViews to fill the UIView in which they are contained.



IV


Now that you have two UIStackViews you can add views that will eventually contain the UILabel and the UITextField.

Add two UIViews to the top horizontal UIStackView. Change their type to UIViewExtended. UIViewExtended will allow you to set the proportions for the view so that it fills the UIStackView in a definable manner.


For the left UIViewExtended set the constraints as shown below.




Also, for the left UIViewExtended set the extended attributes for the intrinsic size (4,0) :



Interface Builder can show you the intrinsic size and how it affects its placement in a UIStackView that is proportionally distributed by setting Intrinsic Size to "PlaceHolder" and enter the exact same value you used for the UIViewExtended "Intrinsic Size".



The place holder is a hint for Interface Builder to render the view using an intrinsic size inside of Interface Builder, it does not set the value for runtime. The runtime value is set because of UIViewExtended.

Now setup the right UIViewExtended.



Set the UIViewExtended intrinsic value as shown above (2, 0). Also setup the constraints and the "PlaceHolder" values as shown below:



Notice that the label is right justified. In the second UIStackView the label will be left justified so that you can see how each works.

Add a UILabel to the left UIViewExtended.





Set the constraints for the UILabel as follows:



Add a UITextField to the UIViewExtended.



Set the UITextField constraints as shown below.






V

Add two UIViews to the second horizontal UIStackView. Change their type to UIViewExtended. UIViewExtended will allow you to set the proportions for the view so that it fills the UIStackView in a definable manner.



For the left UIViewExtended set the Intrinsic size as show above (2,0). Set the constraints for the left UIExtendedView as shown below.



Remember to set the Intrinsic Size "PlaceHolder" as shown above so the InterfaceBuilder will render the view correctly.

Set up the right UIViewExtended as follows.








In the left UIViewExtended ad a UILabel and set it up as follows.



In the right UIViewExtended add a UITextField and set it up as follows.







VI


Now run the "app". Select any iPhone to run, I have started with a smaller device.



Now rotate the device and notice the placements.


Now choose a larger iOS device. I chose iPhone 14 Pro Max.



Now rotate the device.





Conclusion

By using the above approach you can use UIStackViews with proportional distribution and control the placement of a label in relation to a text field. Always there are many ways to solve problems, this is just one way.

Tweak the values until you get a feel for how it all works together.

I wrote this so that I can remember this pattern. My day job is so intense right now that I don't get to work on my iOS apps very often and I forget how I set things up. 

My day job is Java using Apache Solr and Spring Boot apps running in AWS land.