How to use preservesSuperviewLayoutMargins in nested views - ios

I'm trying to make used of preservesSuperviewLayoutMargins through a nested view hierarchy but hitting a lot of issues with UIKit and wondering what I am doing wrong (or whether this is a genuine bug).
I'm trying to lay out a handful of views (some by the side of each other, some above each other) like so:
// Various combinations here will create different visual results (it will either work or won't)
let i1 = ImageView()
//let i1 = LabelView()
let i2 = ImageView()
//let i2 = LabelView()
let i3 = ImageView()
//let i3 = LabelView()
let view = LeftRight(left: i1, right: TopBottom(top: i2, bottom: i3))
//let view = LeftRight(left: TopBottom(top: i2, bottom: i3), right: i1)
With some layout's I can get it to work, shown below. You can see that the images on the right are correctly indented against the green view, which is in turn correctly indented against the white view.
But with others I am struggling:
Running code similar to this in an app I can get a complete lockup and memory runaway.
The indenting is done using layoutMargins like this:
let masterView = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
masterView.translatesAutoresizingMaskIntoConstraints = false
masterView.layoutMargins = UIEdgeInsets(top: 5.0, left: 5.0, bottom: 5.0, right: 5.0)
let subView = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
subView.translatesAutoresizingMaskIntoConstraints = false
subView.layoutMargins = UIEdgeInsets(top: 5.0, left: 5.0, bottom: 5.0, right: 5.0)
Here's a sample of the image layout from a playground. The full playground project can be found here and can easily be loaded up and ran in XCode to see what I mean.
class ImageView: UIView {
init() {
super.init(frame: CGRect(x: 0, y: 0, width: 1000, height: 1000))
self.preservesSuperviewLayoutMargins = true
self.layoutMargins = .zero
let imageView = UIImageView(image: UIImage(named: "jesus"))
imageView.contentMode = .center
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.clipsToBounds = true
self.clipsToBounds = true
imageView.backgroundColor = UIColor.red
self.addSubview(imageView)
imageView.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor).isActive = true
imageView.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor).isActive = true
}
}

I believe that you forgot something those are required.
Implementation of intrinsicContentSize on ImageView and Label
Missing constraints that make both subviews have the same width and height in those 2 UIView subclasses (TopBottom and LeftRight)
right view of view (LeftRight) still haven't been set its translatesAutoresizingMaskIntoConstraints property to false
Add the constraint for bottom of the outmost frame view
Call layoutIfNeeded() on frame view
top.widthAnchor.constraint(equalTo: bottom.widthAnchor).isActive = true
top.heightAnchor.constraint(equalTo: bottom.heightAnchor).isActive = true

Ok. Found your issue. Your master view and your subview have both a frame of size 500x500 and a marginLayout of 5 points per edge. If you instantiate those with a frame of .zero then everything should be fine.
Update
After taking a bit more time with your Playground I also added this to ImageView:
private var imageView: UIImageView!
override var intrinsicContentSize: CGSize {
return imageView?.intrinsicContentSize ?? .zero
}
and LabelView:
private var textLabel: UILabel!
override var intrinsicContentSize: CGSize {
return textLabel?.intrinsicContentSize ?? .zero
}
Now in your init methods of both ImageView and LabelView use these instance properties instead of a local variable. After doing so it seems that all possible combinations work. If you had one specific combination that you knew it didn't please let me know so I can test it.

Related

Auto-sizing a UILabel without setting an explicit height

How do I get a multi-line label to size itself? I don't want to set an explicit height for it but I do need to place it in view.
The way my app is built, we explicitly set frames and origins rather than using NSLayoutConstraints. It's a mature app so this isn't up for discussion.
I'd like to be able to give my UILabel an origin and a width and let it figure its own height out.
How can I do this? This is my playground code:
let view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 180))
view.backgroundColor = .white
let l = UILabel()
l.text = "this is a really long label that should wrap around and stuff. it should maybe wrap 2 or three times i dunno"
l.textColor = .black
l.lineBreakMode = .byWordWrapping
l.numberOfLines = 0
l.textAlignment = .center
l.sizeToFit()
let margin: CGFloat = 60
view
view.addSubview(l)
l.frame = CGRect(x: margin, y: 0, width: view.bounds.width - (margin * 2), height: 100)
// I don't want to do this ^^
This may do what you want...
As requested, you want to set the .origin and .width of a UILabel and have it set its own .height based on the text.
class ZackLabel: UILabel {
override public func layoutSubviews() {
super.layoutSubviews()
let h = sizeThatFits(CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
self.frame.size.height = h.height
}
}
class ViewController: UIViewController {
var testLabel: ZackLabel!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// instantiate a 300 x 180 UIView at 20, 80
let myView = UIView(frame: CGRect(x: 20, y: 80, width: 300, height: 180))
myView.backgroundColor = .white
// instantiate a ZackLabel
testLabel = ZackLabel()
testLabel.text = "this is a really long label that should wrap around and stuff. it should maybe wrap 2 or three times i dunno"
testLabel.textColor = .black
testLabel.lineBreakMode = .byWordWrapping
testLabel.numberOfLines = 0
testLabel.textAlignment = .center
// set background color so we can see its frame
testLabel.backgroundColor = .cyan
let margin: CGFloat = 60
// set label's origin
testLabel.frame.origin = CGPoint(x: margin, y: 0)
// set label's width (label will set its own height)
testLabel.frame.size.width = myView.bounds.width - margin * 2
// add the view
view.addSubview(myView)
// add the label to the view
myView.addSubview(testLabel)
// add a tap recognizer so we can change the label's text at run-time
let rec = UITapGestureRecognizer(target: self, action: #selector(tapFunc(_:)))
view.addGestureRecognizer(rec)
}
#objc func tapFunc(_ sender: UITapGestureRecognizer) -> Void {
testLabel.text = "This is dynamic text being set."
}
}
Result (on an iPhone 8):
and, after tapping on the (yellow) view, dynamically changing the text:
label.sizeThatFits(CGSize(width: <your required width>, height: CGFloat.greatestFiniteMagnitude))
This returns the labels needed size, growing infinitely in height, but fitted to your required width. I've occasionally noticed minor inaccuracies with this function (rounding error?), so I tend to bump the width and height by 1 just to be safe.
UILabel comes with an intrinsic size that should be calculated based on the text and the label's .font property. You may need to add a margin to it...
var height = l.intrinsicContentSize.height
height += margin
l.frame = CGRect(x: margin, y: 0, width: view.bounds.width - (margin * 2), height: height)
Failing that, maybe you can try something like:
let size = CGSize(width: view.bounds.width - (margin * 2), height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
var estimatedFrame = CGRect()
if let font = l.font {
estimatedFrame = NSString(string: l.text).boundingRect(with: size, options: options, attributes: [NSAttributedString.Key.font: font], context: nil)
}
//if you need a margin:
estimatedFrame.height += margin
l.frame = estimatedFrame
Give your UILabel as a UIScrollview or UITableView cell subview.
Then you setup UILabel leading, tralling, top, bottom constrain.
If you give UITableview then set table view hight auto dynamic. If you give UIScrollview
just set UILabel bottom constrain priority low

Give inset to UIImageView before setting Its frame

I have an UITextField component which has an icon on the right side of it. Icon is just an UIImageView. I'm trying to give inset to UIImageView with below code
imageView.image = UIImage(named: "example")?.withAlignmentRectInsets(UIEdgeInsets(top: -4, left: 0, bottom: -4, right: 0))
However, It doesn't work cuz It's frame hasn't been set when setting Its' Image. I'm giving Its frame with below code;
iconImageView.frame = CGRect(
x: bounds.size.width - iconWidth - iconMarginLeft,
y: bounds.size.height - textHeight() - iconMarginBottom,
width: iconWidth,
height: textHeight()
)
and how I setup UIImageView
fileprivate func createIconImageView() {
let iconImageView = UIImageView()
iconImageView.backgroundColor = .clear
iconImageView.contentMode = .scaleAspectFill
iconImageView.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin]
if (hasToolTip) {
let tapGesture = UITapGestureRecognizer(target: self, action:#selector(toolTipTapped(_:)))
iconImageView.addGestureRecognizer(tapGesture)
iconImageView.isUserInteractionEnabled = true
let image = UIImage(named: "infotooltip")
image?.accessibilityIdentifier = "tooltipImage"
iconImageView.image = image!
changeIconAlphaToOne()
self.iconImageView = iconImageView
} else {
self.iconImageView = iconImageView
changeIconAlphaToZero()
}
addSubview(iconImageView)
}
My problem is that, I need to give UIEdgeInset to the image. However, because I'm trying to give it before setting the frame, It doesn't work. How can I achieve this?
I also try to resize the image by referencing below post but doesn't work for me. I also DO NOT want to put it inside a content view which will make my component class more complicated.
The simplest way to resize an UIImage?

Add shadow at bottom of UIview

Actually, I want shadow at bottom of UIView.
I had tried some code but getting shadow from top side only and I am using swift 3 currently.
Please follow below code :
let horizontalLine = UIView()
horizontalLine.frame = CGRect.zero
horizontalLine.backgroundColor = .lightGray
self.addSubview(horizontalLine)
horizontalLine.layer.shadowColor = UIColor.gray.cgColor
horizontalLine.layer.shadowOpacity = 0.5
horizontalLine.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
horizontalLine.layer.masksToBounds = false
horizontalLine.backgroundColor = .lightGray
horizontalLine.layer.shadowRadius = 5
Also I am using snapkit library for UI Setting:
horizontalLine.snp.makeConstraints{ (make) in
make.height.equalTo(5)
make.width.equalTo(self.snp.width)
make.left.equalTo(self.snp.left)
make.right.equalTo(self.snp.right)
make.bottom.equalTo(self.snp.bottom)
}
How it looks now:
Also, I do have collection view just down that.
And we have one more collection view just backside of that line.
Please guide me guys.
Thanks in advance.
Based on the image you show, it looks like the "shadow" you are seeing is in the cell content of the collection view above your horizontalLine view.
It also looks like the shadow on your horizontalLine view is not visible at all - because its superview is clipping it.
Try this:
// new line
self.clipsToBounds = false
// rest of your code...
let horizontalLine = UIView()
try this..
let horizontalLine = UIView()
horizontalLine.frame = CGRect(x: 150, y: 350, width: 150, height: 150)
horizontalLine.backgroundColor = .lightGray
self.view.addSubview(horizontalLine)
horizontalLine.layer.shadowColor = UIColor.red.cgColor
horizontalLine.layer.shadowOffset = CGSize(width: 0.0, height: 3.0)
horizontalLine.layer.shadowOpacity = 1.0
horizontalLine.layer.shadowRadius = 0.0
horizontalLine.layer.masksToBounds = false
horizontalLine.layer.cornerRadius = 4.0

Programmatically display UIScrollView in Swift

I have view controller with views: top level View Contains Container View which contains two views: bottomView, topView, as shown.
Scene
I want to display from: to: date range in the topView. In the bottomView I want to display a UIScrollView that contains two columns which I can scroll. I did that but the topView and BottomView overlap when I introduce scrollView. When I scroll I can see the views getting separated and as soon as i Let go the scrollbar they overlap again.
can someone tell me how to fix it? I just don't seem to understand how the scrollView and bottomView are to be associated.
Code below:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.frame = view.bounds
//scrollView.frame = innerView.bounds
innerView.frame = CGRect(x:0, y:0, width:scrollView.contentSize.width, height:scrollView.contentSize.height)
}
func buildBottomView () {
let screenSize = UIScreen.main.bounds
let screenWidth = screenSize.width
let ht:Int = 21
let incrX:Int = 5
let incrY:Int = 5
let gapCol1:Int = 5
let col1Width:Int = 65
let col2Width:Int = 65
let startY:Int = 5
let col1StartX:Int = 10
let col2StartX:Int = col1StartX + col1Width + gapCol1
var loadRowStartY: Int = 0
// column headers
categoryColumnLabel.text = "Interval"
categoryColumnLabel.font = UIFont.preferredFont(forTextStyle:UIFontTextStyle.subheadline)
//categoryColumnLabel.font = UIFont.systemFont(ofSize: 14)
categoryColumnLabel.frame = CGRect(x: col1StartX, y:startY, width: col1Width, height: ht)
categoryColumnLabel.textAlignment = NSTextAlignment.left
categoryColumnLabel.tag = 1
innerView.addSubview(categoryColumnLabel)
valueColumnLabel.text = "Values"
valueColumnLabel.font = UIFont.preferredFont(forTextStyle:UIFontTextStyle.subheadline)
//valueColumnLabel.font = UIFont.systemFont(ofSize: 14)
valueColumnLabel.frame = CGRect(x: col2StartX, y:startY, width: col2Width, height: ht)
valueColumnLabel.textAlignment = NSTextAlignment.center
valueColumnLabel.tag = 3
innerView.addSubview(valueColumnLabel)
let sepLine:UIView = UIView()
sepLine.frame = CGRect(x: col1StartX, y:startY+ht+incrY, width: Int(screenWidth-20), height: 2)
sepLine.backgroundColor = UIColor.darkGray
sepLine.tag = 60
loadRowStartY = startY+ht+incrX+ht
innerView.addSubview(sepLine)
for i in 0 ..< 24 {
let timeIntervalLabel = UILabel()
let value2Label = UILabel()
print("display load profile")
let loadStruct = loadDict[String(i)] as! CommercialProfile
print (loadStruct.timeInterval)
timeIntervalLabel.text = loadStruct.timeInterval
timeIntervalLabel.font = UIFont.preferredFont(forTextStyle:UIFontTextStyle.caption1)
//valueColumnLabel.font = UIFont.systemFont(ofSize: 14)
timeIntervalLabel.frame = CGRect(x: col1StartX, y:loadRowStartY, width: col1Width, height: Int(timeIntervalLabel.font.lineHeight))
timeIntervalLabel.textAlignment = NSTextAlignment.center
innerView.addSubview(timeIntervalLabel)
print(loadStruct.value)
value2Label.text = loadStruct.value
value2Label.font = UIFont.preferredFont(forTextStyle:UIFontTextStyle.caption1)
//value2Label = UIFont.systemFont(ofSize: 14)
value2Label.frame = CGRect(x: col2StartX, y:loadRowStartY, width: col2Width, height: Int(value2Label.font.lineHeight))
value2Label.textAlignment = NSTextAlignment.center
innerView.addSubview(value2Label)
loadRowStartY = loadRowStartY + incrY + Int(value2Label.font.lineHeight)
}
you are setting the bounds of the scrollView to the size of the whole view with this code: scrollView.frame = view.bounds.
The scrollView only needs to scroll the content in the bottom view. Scroll Views have their own content, that is normally larger than the viewable area of the screen/view. The scroll view just allows you to pan the viewport of that view.
So add the bottom view and setup your constraints on that. add the scrollView to the bottom view and then add your content into the scrollView.
Make sure that your bottom view has clipToBounds set to true and then you should be able to keep the headers in place and just scroll the content.
I'll try and put an example together for you shortly.
EDIT:
I've just created this simple example which shows the scroll behaviour you need. This works in a playground or just as a simple view controller. I've intentionally not used auto layout or setup constraints due to time, but you will see what you need to solve your issue
class ViewController: UIViewController {
var topView: UIView!
var bottomView: UIView!
var scrollView: UIScrollView!
var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let screenSize = UIScreen.main.bounds
self.topView = UIView(frame: CGRect(x: 0, y: 0, width: screenSize.width, height: 100))
self.bottomView = UIView(frame: CGRect(x: 0, y: 100, width: screenSize.width, height: screenSize.height - 100))
self.scrollView = UIScrollView(frame: CGRect(x: 0, y: 100, width: screenSize.width, height: screenSize.height - 100))
self.contentView = UIView(frame: CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height * 3))
self.view.backgroundColor = .white
self.view.addSubview(self.topView)
self.view.addSubview(self.bottomView)
self.bottomView.addSubview(self.scrollView)
self.scrollView.addSubview(self.contentView)
self.bottomView.clipsToBounds = true
self.scrollView.contentSize = CGSize(width: screenSize.width, height: screenSize.height * 3)
self.contentView.backgroundColor = .gray
}
}

How to embed stack view in scroll view programmatically

I have tried embedding it, but my stack view is dynamic and my app is also changing orientations time to time. I have segment control at the end of the view.
I have also tried googling it but had no luck. thanks in advance.
So far I have done:
In view did load:
mainStackView.axis = UILayoutConstraintAxis.Vertical
mainStackView.spacing = 3
scrollView.frame = self.view.bounds
scrollView.addSubview(mainStackView)
view.addSubview(scrollView)
In view did layout:
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
let top = topLayoutGuide.length
let bottom = bottomLayoutGuide.length
self.mainStackView.frame = CGRect(x: 0, y: top, width: view.frame.width, height: view.frame.height - top - bottom).insetBy(dx: 10, dy: 10)
dispatch_async(dispatch_get_main_queue())
{
self.scrollView.frame = self.view.bounds
self.scrollView.contentSize = CGSize(width: self.view.bounds.width, height: self.segmentedControl.frame.origin.y + self.segmentedControl.frame.height + 50)
}
print(scrollView.contentSize)
}
You need to set the height constraint of segment control.
For Example:
segmentedControl.heightAnchor.constraintEqualToConstant(50).active = true
More over, you can Add Empty bottom view to avoid stack view's must fill mechanism. This will show you desired view output.
var bottomView = UIView(frame: CGRectZero)
stackView.addArrangedSubview(bottomView)

Resources