UIScrollView Subviews being misplaced when scaled down and Subviews not showing up when rounded - ios

so I'm making my first Swift iOS app. I have two problems that I would like some help with.
First problem: I'm running into some issues with subviews that I created programmatically not being aligned properly when displayed on a small screen.
This first picture shows how I intend the scrollview to look. (iPhone 11 Pro Max)
However, this is what it looks like when it's on a smaller screen. (iPhone 8)
I think the issue is with me creating the subviews programmatically as the scrollview's size is what I want it to be.
Here's the swift code that I used inside viewDidLoad():
var districtNewsSize = 10;
var districtNewsFrame = CGRect(x:0,y:0,width:0,height:0);
// District News -----
districtNewsPageControl.numberOfPages = districtNewsSize;
for aIndex in 0..<districtNewsSize{
districtNewsFrame.origin.x = districtNewsScrollView.frame.size.width * CGFloat(aIndex);
districtNewsFrame.size = districtNewsScrollView.frame.size;
// create content in scrollview
let contentView = UIButton(frame: districtNewsFrame);
contentView.backgroundColor = makeColor(r: 147, g: 66, b: 78);
contentView.setTitle("titletext", for: .normal);
contentView.layer.cornerRadius = 20; // just to show how displaced the views are
// add contentview to scrollview
self.districtNewsScrollView.addSubview(contentView);
}
// change horizontal size of scrollview
districtNewsScrollView.contentSize = CGSize(width: districtNewsScrollView.frame.size.width * CGFloat(districtNewsSize), height: districtNewsScrollView.frame.size.height);
districtNewsScrollView.delegate = self;
Second Problem: When attempting to round the top two corners of the scrollview, the content inside the scrollview seems to disappear.
This is a picture of the first subview.
This is a picture of the second subview.
I also have a suspicion that this is caused by my function that I am using to round the top edges.
Here's the extension function I made for UIScrollView:
extension UIScrollView{
func setRoundedEdge(corners:UIRectCorner, radius: CGFloat){ // label.setRoundedEdge([.TopLeft, . TopRight], radius: 10)
let maskPath1 = UIBezierPath(roundedRect: bounds,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = bounds
maskLayer1.path = maskPath1.cgPath
layer.mask = maskLayer1
}
}
This is how I'm calling the function in viewDidLoad():
districtNewsScrollView.setRoundedEdge(corners: [.topRight,.topLeft], radius: 30);
Any help will greatly be appreciated. I've been stuck on these two problems and can't find anything online (probably because I'm not wording it right but I don't know how to describe these issues). Thank you!

I assume you are using storyboard for scrollview and programmatically adding subviews in viewDidLoad,
districtNewsFrame.origin.x = districtNewsScrollView.frame.size.width * CGFloat(aIndex);
districtNewsFrame.size = districtNewsScrollView.frame.size;
Should be changed to
districtNewsFrame.origin.x = UIScreen.main.bounds.size.width * CGFloat(aIndex);
districtNewsFrame.size = UIScreen.main.bounds.frame.size;
Use screen's size on viewDidLoad since the views in storyboards are loaded but constraints in auto layout is not yet applied.
For second question,
change extension to
extension UIScrollView{
func setRoundedEdge(corners:UIRectCorner, radius: CGFloat){ // label.setRoundedEdge([.TopLeft, . TopRight], radius: 10)
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
and view controller to
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews();
districtNewsScrollView.setRoundedEdge(corners: [.topRight,.topLeft], radius: 30);
}

Here's what I did to fix both issues:
// District News -----
districtNewsFrame.size = districtNewsScrollView.frame.size;
districtNewsFrame.size.width = UIScreen.main.bounds.size.width - 34;
for aIndex in 0..<districtNewsSize{
//districtNewsFrame.origin.x = (UIScreen.main.bounds.size.width-52) * CGFloat(aIndex);
//districtNewsFrame.size = UIScreen.main.bounds.size;
districtNewsFrame.origin.x = (districtNewsFrame.size.width * CGFloat(aIndex));
// create content in scrollview
let contentView = UIButton(frame: districtNewsFrame); // wrapper for article
//testButton.setImage(UIImage(named: "ahsldpi.png"), for: .normal);
contentView.backgroundColor = makeColor(r: 147, g: 66, b: 78);
// content inside contentView -------
let articleContentFrame = CGRect(x:0,y:0,width:districtNewsFrame.width,height:districtNewsFrame.height-60);
let articleContent = UIView(frame: articleContentFrame); // may be image?
articleContent.backgroundColor = makeColor(r: 138, g: 138, b: 138);
let articleLabelFrame = CGRect(x:0,y:districtNewsFrame.height-60,width:districtNewsFrame.width,height:60);
let articleLabel = UILabel(frame: articleLabelFrame);
articleLabel.text = " " + "Title";
articleLabel.backgroundColor = UIColor.white;
articleLabel.font = UIFont(name: "DINCondensed-Bold", size: 30);
// add content inside contentView to contentview
contentView.addSubview(articleContent);
contentView.addSubview(articleLabel);
// add contentview to scrollview
contentView.setRoundedEdge(corners: [.topRight,.topLeft,.bottomLeft,.bottomRight], radius: 30);
self.districtNewsScrollView.addSubview(contentView);
}
// change horizontal size of scrollview
districtNewsScrollView.contentSize = CGSize(width: (districtNewsFrame.size.width * CGFloat(districtNewsSize)), height: districtNewsScrollView.frame.size.height);
districtNewsScrollView.delegate = self;
```

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

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

Why is my table view shadow scrolling with my table view?

I added shadow to my table view but unfortunately when I scroll through the table view the shadow also moves with the table. The code for adding shadow is as follows:
func addShadow(to myView: UIView){
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: uiView.frame.width, height: uiView.frame.height * 1.1))
uiView.layer.shadowColor = UIColor.black.cgColor
uiView.layer.shadowOffset = CGSize(width: 0.2, height: 0)
uiView.layer.shadowOpacity = 0.5
uiView.layer.shadowRadius = 5.0
uiView.layer.masksToBounds = false
uiView.layer.shadowPath = shadowPath.cgPath
}
Can you please explain to me why is this happening and how to make the shadow stick to its designated location?
Thank you in advance.
If you are adding shadow on tableView, it will scroll along with tableview data. To prevent that you have to add UIView first. Add tableview on that view. Add shadow for the UIView you have taken. It will stick to designated location.
This is my solution in Swift 3 with an UIView and a CAGradientLayer inside.
override func viewWillAppear(_ animated: Bool) {
addShadow(myView: myTab)
}
func addShadow(myView: UIView){
let shadowView = UIView()
shadowView.center = CGPoint(x: myView.frame.minX,y:myView.frame.minY - 15)
shadowView.frame.size = CGSize(width: myView.frame.width, height: 15)
let gradient = CAGradientLayer()
gradient.frame.size = shadowView.frame.size
let stopColor = UIColor.clear.cgColor
let startColor = UIColor.black.withAlphaComponent(0.8).cgColor
gradient.colors = [stopColor,startColor]
gradient.locations = [0.0,1.0]
shadowView.layer.addSublayer(gradient)
view.addSubview(shadowView)
}

Stack View ScaleAspectFit Mask Resize in Swift

I am masking an image within a stack view and for some odd reason, my mask is not aligning/resizing correctly with the image.
Here is a demonstration of what's occurring as I'm dynamically adding instances of this image in a stack view while each subview is resized within its boundaries and spacing.
As you can see, the mask retains the original size of the image and not the resized version. I've tried many different width & height variations including the bounds.width, layer.frame.width, frame.width, frame.origin.x, etc, and had no luck.
Current code in Swift 2:
let testPicture:UIImageView = UIImageView(image: UIImage(named: "myPicture"))
testPicture.contentMode = .ScaleAspectFit
testPicture.layer.borderWidth = 1
testPicture.clipsToBounds = true
testPicture.layer.masksToBounds = true
view.layer.addSublayer(shapeLayer)
var width = testPicture.layer.frame.width
var height = testPicture.layer.frame.height
let center = CGPointMake(width/2, height/2)
let radius = CGFloat(CGFloat(width) / 2)
// Mask
let yourCarefullyDrawnPath = UIBezierPath()
yourCarefullyDrawnPath.moveToPoint(center)
yourCarefullyDrawnPath.addArcWithCenter(center,
radius: radius,
startAngle: 0,
endAngle: CGFloat( (0.80*360.0) * M_PI / 180.0),
clockwise: true)
yourCarefullyDrawnPath.closePath()
let maskPie = CAShapeLayer()
maskPie.frame = testPicture.layer.bounds
testPicture.clipsToBounds = true
testPicture.layer.masksToBounds = true
maskPie.path = yourCarefullyDrawnPath.CGPath
testPicture.layer.mask = maskPie
// Add Into Stackview
self.myStackView.addArrangedSubview(testPicture)
self.myStackView.layoutIfNeeded()
I suspect that I'm fetching the wrong width and height in order to generate the center and radius variables although after trying all the different widths and heights I can find, I still cannot achieve the correct sizes. :-(
You'll want to get the frame the image occupies within the image view.
Unfortunately, UIImageView provides no native support for doing this, however you can calculate this fairly simply. I have already created a function that will take a given outer rect, and a given inner rect and return the inner rect after it's been aspect fitted to sit within the outer rect.
A Swift version of the function would look something like this:
func aspectFitRect(outerRect outerRect:CGRect, innerRect:CGRect) -> CGRect {
let innerRectRatio = innerRect.size.width/innerRect.size.height; // inner rect ratio
let outerRectRatio = outerRect.size.width/outerRect.size.height; // outer rect ratio
// calculate scaling ratio based on the width:height ratio of the rects.
let ratio = (innerRectRatio > outerRectRatio) ? outerRect.size.width/innerRect.size.width:outerRect.size.height/innerRect.size.height;
// The x-offset of the inner rect as it gets centered
let xOffset = (outerRect.size.width-(innerRect.size.width*ratio))*0.5;
// The y-offset of the inner rect as it gets centered
let yOffset = (outerRect.size.height-(innerRect.size.height*ratio))*0.5;
// aspect fitted origin and size
let innerRectOrigin = CGPoint(x: xOffset+outerRect.origin.x, y: yOffset+outerRect.origin.y);
let innerRectSize = CGSize(width: innerRect.size.width*ratio, height: innerRect.size.height*ratio);
return CGRect(origin: innerRectOrigin, size: innerRectSize);
}
The other thing you need to do is subclass UIImageView and override the layoutSubviews method. This is because as you're adding your image views to a UIStackView - you're no longer in control of the frames of your image views. Therefore by overriding layoutSubviews, you'll be able to update your mask whenever the stack view alters the frame of the view.
Something like this should achieve the desired result:
class MaskedImageView: UIImageView {
let maskLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
override init(image: UIImage?) {
super.init(image: image)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// configure your common image view properties here
contentMode = .ScaleAspectFit
clipsToBounds = true
// mask your image layer
layer.mask = maskLayer
}
override func layoutSubviews() {
guard let img = image else { // if there's no image - skip updating the mask.
return
}
// the frame that the image itself will occupy in the image view as it gets aspect fitted
let imageRect = aspectFitRect(outerRect: bounds, innerRect: CGRect(origin: CGPointZero, size: img.size))
// update mask frame
maskLayer.frame = imageRect
// half the image's on-screen width or height, whichever is smallest
let radius = min(imageRect.size.width, imageRect.size.height)*0.5
// the center of the image rect
let center = CGPoint(x: imageRect.size.width*0.5, y: imageRect.size.height*0.5)
// your custom masking path
let path = UIBezierPath()
path.moveToPoint(center)
path.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI*2.0*0.8), clockwise: true)
path.closePath()
// update mask layer path
maskLayer.path = path.CGPath
}
}
You can then create your image views from your view controller and add them to your stack view as normal.
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
stackView.frame = view.bounds
stackView.distribution = .FillProportionally
stackView.spacing = 10
view.addSubview(stackView)
for _ in 0..<5 {
let imageView = MaskedImageView(image:UIImage(named:"foo.jpg"))
stackView.addArrangedSubview(imageView)
stackView.layoutIfNeeded()
}
}
Gives me the following result:
Unrelated Ramblings...
Just noticed in your code that you're doing this:
testPicture.clipsToBounds = true
testPicture.layer.masksToBounds = true
These both do the same thing.
A UIView is no more than a wrapper for an underlying CALayer. However for convenience, some CALayer properties also have a UIView equivalent. All the UIView equivalent does is forward to message down to the CALayer when it is set, and retrieve a value from the CALayer when it is 'get'ed.
clipsToBounds and masksToBounds are one of these pairs (although annoyingly they don't share the same name).
Try doing the following:
view.layer.masksToBounds = true
print(view.clipsToBounds) // output: true
view.layer.masksToBounds = false
print(view.clipsToBounds) // output: false
view.clipsToBounds = true
print(view.layer.masksToBounds) // output: true
view.clipsToBounds = false
print(view.layer.masksToBounds) // output: false
Seeing as you're working with a UIView, clipToBounds is generally the preferred property to update.

ios: Center view in its superview

I'm trying to center my subview with a button in itssuperview. So I want the center of the subview be the center of the superview. I'm trying that with following code:
override func viewDidLoad() {
self.view.backgroundColor = UIColor.redColor()
var menuView = UIView()
var newPlayButton = UIButton()
//var newPlayImage = UIImage(named: "new_game_button_5cs")
var newPlayImageView = UIImageView(image: UIImage(named: "new_game_button_5cs"))
newPlayButton.frame = CGRectMake(0, 0, newPlayImageView.frame.width, newPlayImageView.frame.height)
newPlayButton.setImage(newPlayImage, forState: .Normal)
newPlayButton.backgroundColor = UIColor.whiteColor()
menuView.center = self.view.center
menuView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2)
menuView.backgroundColor = UIColor.whiteColor()*/
menuView.addSubview(newPlayButton)
}
Unfortunately it doesent seem to work as this is the result:
UIView *subview = your View To Be Centered In Its SuperView;
UIView *superView = subview.superview;
subview.center = [superView convertPoint:superView.center
fromView:superView.superview];
If view is nil(on fromView:), this method instead converts from window base coordinates. Otherwise, both view and the receiver must belong to the same UIWindow object.
NOTE: If you use the auto layout stuff, then you have to change the constraints . not the frame or center.
Good Luck :)
Try removing your view width from your superview width, e.g.:
var width: CGFloat = (self.view.bounds.size.width / 2)
// (here goes your view width that you want centralize)
menuView.center = CGPointMake(width, self.view.bounds.size.height / 2)
My working code
vRate = superview
rcRating = view ( that I want to centralize in vRate )
self.rcRating = AMRatingControl(location: CGPoint(x: 0, y: 0), andMaxRating: 5)
self.vRate.addSubview(self.rcRating)
var width: CGFloat = (self.vRate.bounds.size.width / 2) - self.rcRating.bounds.size.width
self.rcRating.center = CGPointMake(width, self.vRate.bounds.size.height / 2)

Resources