Swift UIView Subviews not rounding corners in Custom UIView Subclass - ios

Greetings stack overflow.
I am trying to build a "bullseye" type view, using coloured subviews and the corner radius. The problem I have is, only my first subview's corners are getting rounded and the inner views are still squares. The black view is a subview of my custom view. The red view is it's subview, and they yellow view the subview of that. Pretty simple hierarchy.
The result looks like this:
I add the views and set their constraints manually. My test app just has the ThreeCircleView dead center of a view controller with the X,Y centered and the width, height constant. I do the actual rounding of the corners in didLayoutSubViews because the size of the view might change, so the corners would have to be resized.
I wrote a test view to isolate this, here it is
class ThreeCircleView: UIView {
var outerCircle: UIView = UIView()
var middleCircle: UIView = UIView()
var innerCircle: UIView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
addSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
translatesAutoresizingMaskIntoConstraints = false
addSubViews()
}
func addSubViews() {
outerCircle.backgroundColor = .black
middleCircle.backgroundColor = .red
innerCircle.backgroundColor = .yellow
self.addSubview(outerCircle)
outerCircle.addSubview(middleCircle)
middleCircle.addSubview(innerCircle)
let outerCenterY = outerCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let outerCenterX = outerCircle.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let outerCenterWidth = outerCircle.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -50.0 )
let outerCenterHeight = outerCircle.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -50.0 )
outerCircle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([outerCenterY,outerCenterX,outerCenterWidth,outerCenterHeight])
self.setNeedsLayout()
let middleCenterY = middleCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let middleCenterX = middleCircle.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let middleCenterWidth = middleCircle.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -100.0 )
let middleCenterHeight = middleCircle.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -100.0 )
middleCircle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([middleCenterY,middleCenterX,middleCenterWidth,middleCenterHeight])
let innerCenterY = innerCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let innerCenterX = innerCircle.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let innerCenterWidth = innerCircle.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -150.0 )
let innerCenterHeight = innerCircle.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -150.0 )
innerCircle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([innerCenterY,innerCenterX,innerCenterWidth,innerCenterHeight])
}
func makeCircle(v:UIView) {
v.layer.cornerRadius = v.frame.size.width * 0.50
v.clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
makeCircle(v: outerCircle)
makeCircle(v: middleCircle)
makeCircle(v: innerCircle)
}
}

An easy way to make it look as expected is to add layoutIfNeeded() call inside your makeCircle(v:UIView) method. This will make you sure that all views' frames are updated correctly before applying visual changes:
func makeCircle(v:UIView) {
v.layoutIfNeeded()
v.layer.cornerRadius = v.frame.size.width * 0.50
v.clipsToBounds = true
}

Related

How to put UIView inside UIScrollView in Swift?

I am trying to add a UIView into a UIScrollView without storyboard. I made some code with the given two files(CalcTypeView.siwft and CalcTypeViewController.swift) as below. However, as shown in the screenshot image, I can see the UIScrollView(gray color) while UIView(red color) does not appear on the screen. What should I do more with these code to make UIView appear? (I've already found many example code using single UIViewController, but what I want is UIView + UIViewController form to maintain MVC pattern)
1. CalcTypeView.swift
import UIKit
final class CalcTypeView: UIView {
private let scrollView: UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .gray
view.showsVerticalScrollIndicator = true
return view
}()
private let contentView1: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
view.clipsToBounds = true
view.layer.cornerRadius = 10
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupScrollView()
setupContentView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupScrollView() {
self.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
scrollView.widthAnchor.constraint(equalTo: self.widthAnchor),
scrollView.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor),
])
}
private func setupContentView() {
scrollView.addSubview(contentView1)
NSLayoutConstraint.activate([
contentView1.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 20),
contentView1.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -20),
contentView1.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 20),
contentView1.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -20),
])
}
}
2. CalcTypeViewController.swift
import UIKit
final class CalcTypeViewController: UIViewController {
private let calcTypeView = CalcTypeView()
override func viewDidLoad() {
super.viewDidLoad()
setupNavBar()
setupView()
}
private func setupNavBar() {
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithOpaqueBackground()
navigationBarAppearance.shadowColor = .clear
navigationController?.navigationBar.standardAppearance = navigationBarAppearance
navigationController?.navigationBar.scrollEdgeAppearance = navigationBarAppearance
navigationController?.navigationBar.tintColor = Constant.ColorSetting.themeColor
navigationController?.navigationBar.prefersLargeTitles = false
navigationController?.setNeedsStatusBarAppearanceUpdate()
navigationController?.navigationBar.isTranslucent = false
navigationItem.scrollEdgeAppearance = navigationBarAppearance
navigationItem.standardAppearance = navigationBarAppearance
navigationItem.compactAppearance = navigationBarAppearance
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "bookmark.fill"), style: .plain, target: self, action: #selector(addButtonTapped))
navigationItem.rightBarButtonItem?.tintColor = Constant.ColorSetting.themeColor
navigationItem.title = Constant.MenuSetting.menuName2
self.extendedLayoutIncludesOpaqueBars = true
}
override func loadView() {
view = calcTypeView
}
private func setupView() {
view.backgroundColor = .systemBackground
}
#objc private func addButtonTapped() {
let bookmarkVC = BookmarkViewController()
navigationController?.pushViewController(bookmarkVC, animated: true)
}
}
My Screenshot
A scroll view's contentLayoutGuide defines the size of the scrollable area of the scroll view. The default size is 0,0.
In your code your contentView1 has no intrinsic size. It simply has a default size of 0,0. So your constraints are telling the scroll view to make its contentLayoutGuide to be 40,40 (based on the 20 and -20 constants) and leave the contentView1 size as 0,0.
If you setup contentView1 with specific width and height constraints then the scroll view's content size would be correct so that contentView1 would scroll within the scroll view.
A better example might be to add a UIStackView with a bunch of labels. Since the stack view will have an intrinsic size based on its content and setup, the contentLayoutGuide of the scroll view will fit around the stack view's intrinsic size.

Why does my UIView.Transition not work unless wrapped in a UIView.Animate block?

I am using a UIView.transition to flip a card over.
While troubleshooting it not working, I stumbled upon a way to make it work - but I have no idea why. I am hoping that someone can look at the two code blocks below and help me understand why one works and the other doesn't. It seems very strange to me.
First, here is the code block that actually works. The card flip is visually flawless.
UIView.animate(withDuration: 0.01) {
imageView.alpha = 1.0
imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded()
} completion: { (true) in
UIView.transition(with: imageView, duration: 1.2, options: animation) {
imageView.image = endingImage
imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded()
} completion: { (true) in
if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 }
UIView.animate(withDuration: 0.01) {
self.coveringLabel.backgroundColor = .clear
self.coveringLabel.layoutIfNeeded()
imageView.removeFromSuperview()
self.tickNumLabel.alpha = originalTicketNumAlpha
}
}
}
But I don't understand why I seem to need to wrap the UIView.transition() into the completion handler of a call to UIView.animate() in order for the flip animation to work.
*(Note: If I pull the "imageView.alpha = 1.0" out of the animate() block and place it immediately BEFORE calling UIView.animate() - the flip animation does not occur (with or without the layoutIfNeeded() call). It just toggles the images. *
Now, here is the code that I expected to work - but when I use this code instead of the above code, there is no "flip" transition. The card image just immediately toggles between the face up and face down image. The "UIView.transition" call here is identical to the one in the above code. The only difference here is that it's NOT wrapped into a 0.01 second UIView.animate completion block.
imageView.alpha = 1.0
imageView.layoutIfNeeded()
UIView.transition(with: imageView, duration: 1.2, options: animation) {
imageView.image = endingImage
imageView.layoutIfNeeded() // same behaviour with and without this line
} completion: { (true) in
if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 }
UIView.animate(withDuration: 0.01) {
self.coveringLabel.backgroundColor = .clear
self.coveringLabel.layoutIfNeeded()
imageView.removeFromSuperview()
self.tickNumLabel.alpha = originalTicketNumAlpha
}
}
This transition ends my flipTicket() function. The code that precedes this transition is identical in both cases. I wasn't going to include it because I don't think it's necessary to understand the problem - but then again - what do I know? Here's the stuff that came before the above clips:
func flipTicket() {
let originalTicketNumAlpha = self.tickNumLabel.alpha
self.tickNumLabel.alpha = 0.0
let tempFaceDownImage:UIImage = self.dealTicketState.faceDownImage
let tempFaceUpImage:UIImage = getCurrentFaceUpImage()
var endingImage:UIImage = self.dealTicketState.faceDownImage
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
imageView.alpha = 0.0
self.coveringLabel.alpha = 1.0
self.coveringLabel.backgroundColor = .black
self.coveringLabel.layoutIfNeeded()
var animation:UIView.AnimationOptions = .transitionFlipFromLeft
if faceDownView.alpha == 1.0 {
animation = .transitionFlipFromRight
imageView.image = tempFaceDownImage
endingImage = tempFaceUpImage
} else {
animation = .transitionFlipFromLeft
imageView.image = tempFaceUpImage
}
self.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: self.topAnchor),
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
imageView.layoutIfNeeded()
Background:
This code is part of a custom UI control which represents a playing card. It consists of several subviews. The topmost subview was originally a UIImageView which held the image of the back of the card. This let me simply toggle the alpha for that top view to display the card as either face up or face down. And then I added one more topMost view to the control - a UILabel with "background = .clear" and attached a UITapGestureRecognizer to it. When the control is tapped, this function which is meant to animate the flipping over of the card is called.
To construct the animation, I call the getCurrentFaceUp() function which temporarily sets the alpha of the card's faceDownView to 0 (so I can take a snapshot of the card underneath it as it is currently configured). It returns a UIImage of the "face up" view of the card. I already have a UIImage of the faceDown view. These are the 2 images I need for the transition.
So...then I set the background color of that topMost UILabel to .black, create a new temporary UIImageView and place it on top of the existing control. I set the temporary imageView to initially display whichever one of the 2 images is currently visible on the control. And then I run the flip transition, change the configuration of the background control to match the new state, change the label background back to .clear and dispose of the temporary UIImageView.
(If there's a better way to accomplish this, I'm open to hearing it but the main purpose of this post is to understand why my code appears to be acting strangely.)
When I was looking for a way to animate a card flip, I found a YouTube video that demonstrated the UIView.transition() with the flip animation. It did not require using a UIView.animate() wrapper in order to make it work - so I'm pretty certain that it's me that did something wrong -but I've spent hours testing variations and searching for someone else with this problem and I haven't been able to find the answer.
Thanks very much in advance to anybody that can help me understand what's going on here...
A little tough to tell (didn't try to actually run your code), but I think you may be doing more than you need to.
Take a look at this...
We'll start with two "Card View" subclasses - Front and Back (we'll "style" them in the next step):
class CardFrontView: UIView {
}
class CardBackView: UIView {
}
Then, a "Playing Card View" class, that contains a "Front" view (cyan) and a "Back" view (red). On init, we add the subviews and set the "Front" view hidden. On tap, we'll run the flip transition between the Front and Back views:
class PlayingCardView: UIView {
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .cyan
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
#objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and then here's a simple Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
}
}
The result:
So, next step, we'll add a little styling to the Front and Back views -- no changes to the PlayingCardView functionality... just a couple new lines to set the styling...
Card Front View - with rounded corners, a border and labels at the corners and center:
class CardFrontView: UIView {
var theLabels: [UILabel] = []
var cardID: Int = 0 {
didSet {
theLabels.forEach {
$0.text = "\(cardID)"
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
for i in 1...5 {
let v = UILabel()
v.font = .systemFont(ofSize: 24.0)
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
switch i {
case 1:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 2:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 4:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
default:
v.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
v.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
theLabels.append(v)
}
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
}
}
Looks like this:
Card Back View - with rounded corners, a border and a cross-hatch pattern:
class CardBackView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
layer.masksToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
// simple cross-hatch pattern
let hReplicatorLayer = CAReplicatorLayer()
let vReplicatorLayer = CAReplicatorLayer()
let line = CAShapeLayer()
let pth = UIBezierPath()
pth.move(to: CGPoint(x: 0.0, y: 0.0))
pth.addLine(to: CGPoint(x: 20.0, y: 20.0))
pth.move(to: CGPoint(x: 20.0, y: 0.0))
pth.addLine(to: CGPoint(x: 0.0, y: 20.0))
line.strokeColor = UIColor.yellow.cgColor
line.lineWidth = 1
line.path = pth.cgPath
var instanceCount = Int((bounds.maxX + 0.0) / 20.0)
hReplicatorLayer.instanceCount = instanceCount
hReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)
instanceCount = Int((bounds.maxY + 0.0) / 20.0)
vReplicatorLayer.instanceCount = instanceCount
vReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, 20, 0)
hReplicatorLayer.addSublayer(line)
vReplicatorLayer.addSublayer(hReplicatorLayer)
layer.addSublayer(vReplicatorLayer)
}
}
Looks like this:
Playing Card View - only change is setting the Front Card background color to white, and setting its "ID" to 5:
class PlayingCardView: UIView {
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .white
cardFront.cardID = 5
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
#objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and finally, the same Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
}
}
and here's the new result:

Swift: Is there a faster, more responsive way to implement different corner radii for a UIView?

Currently, I implement multiple corner radii on my bubbleView which is a UIView by doing something along the lines of:
// First create the bubble view
bubbleView = UIView()
bubbleView.layer.cornerRadius = 4 // set the corner radius of the "smaller" corner style
bubbleView.layer.cornerCurve = .continuous
bubbleView.clipsToBounds = true
bubbleView.backgroundColor = UIColor.systemBlue
// ...
// Update the "mask" of the bubble view to give another type of rounded corners
let maskPath = UIBezierPath(roundedRect:bubbleView.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 17.0, height: 0.0))
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.cgPath
bubbleView.layer.mask = maskLayer // updates the mask
My issue is I am setting the mask, self.bubbleView.layer.mask = maskLayer, of the bubbleView in the func layoutSubviews() function, which causes a noticable delay, for example, when the device rotates from portrait to landscape mode.
Is there a faster, more efficient way to implement different corner radii for a UIView that responds faster than simply updating the mask in layoutSubviews() ?
You could try using a subview with the larger radius corners...
custom view
clear background
all 4 corners set to Radius of 4
subView with desired background color
set Radius of 17 on desired corners of subView
Here's some sample code:
class MyCustomView: UIView {
// self's background will be .clear
// so we use a custom property to set the
// background of the subView
public var viewColor: UIColor = .clear {
didSet {
subView.backgroundColor = viewColor
}
}
// corners to use larger radius
public var corners: CACornerMask = [] {
didSet {
subView.layer.maskedCorners = corners
}
}
public var smallRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = smallRadius
}
}
public var bigRadius: CGFloat = 0 {
didSet {
subView.layer.cornerRadius = bigRadius
}
}
private let subView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(subView)
subView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
subView.topAnchor.constraint(equalTo: topAnchor),
subView.leadingAnchor.constraint(equalTo: leadingAnchor),
subView.trailingAnchor.constraint(equalTo: trailingAnchor),
subView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
// round all 4 corners of self's layer with the small radius
layer.masksToBounds = true
layer.cornerRadius = smallRadius
layer.cornerCurve = .continuous
// subview only specified corners with bigger radius
subView.layer.masksToBounds = true
subView.layer.cornerRadius = bigRadius
subView.layer.cornerCurve = .continuous
subView.layer.maskedCorners = corners
}
}
and a test view controller to demo it:
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let v = MyCustomView()
v.viewColor = .systemBlue
v.smallRadius = 4
v.bigRadius = 17
// set top-left, top-right, bottom-left to use larger radius
v.corners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner]
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
v.heightAnchor.constraint(equalToConstant: 120.0),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}

UIView internal constraints

I have created a simple UIView that contains a red box (UIImage) at its centre. When all the constraints are constants the code works fine. However, if I replace the height constraint with one that makes the box half the height of the view then the box disappears.
I assume that this is either because I am doing it wrong (obviously) or I need to do something more to force the constraint to realise the UIView height is greater than zero.
How do I set the redBox height constraint so that it is always half the height of the BoxView?
import UIKit
class BoxView: UIView {
public var redBox: UIImageView
public override init(frame: CGRect) {
redBox = UIImageView(frame: .zero)
redBox.backgroundColor = .red
super.init(frame: frame)
self.backgroundColor = .yellow
addSubview(redBox)
redBox.translatesAutoresizingMaskIntoConstraints = false
let margins = layoutMarginsGuide
redBox.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
redBox.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
redBox.widthAnchor.constraint(equalToConstant: 100).isActive = true
//redBox.heightAnchor.constraint(equalToConstant: 100).isActive = true
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, constant: 0.5)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view = BoxView()
}
}
Replace
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, constant: 0.5)
with
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5).isActive = true
NSLayoutConstraint.activate([
redBox.centerXAnchor.constraint(equalTo: self.centerXAnchor),
redBox.centerYAnchor.constraint(equalTo: self.centerYAnchor),
redBox.widthAnchor.constraint(equalToConstant: 100),
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5)
])
In your current code first you miss the .isActive = true which has the same effect as if the line doesn't exist , and if specified this will make the box height equal to the view's height + constant ( = 0.5 )
box height = view height * multiplier + constant
and since default multiplier = 1 and you set constant = 0.5 this will be
box height = view height * 1.0 + 0.5
But instead you need
box height = view height * 0.5 + 0 // omit consatnt in constraint and it will be zero
class BoxView: UIView {
public var redBox: UIImageView
public override init(frame: CGRect) {
super.init(frame: frame)
redBox = UIImageView(frame: .zero)
redBox.backgroundColor = .red
self.backgroundColor = .yellow
addSubview(redBox)
redBox.translatesAutoresizingMaskIntoConstraints = false
let margins = layoutMarginsGuide
NSLayoutConstraint.activate([
redBox.centerXAnchor.constraint(equalTo: self.centerXAnchor),
redBox.centerYAnchor.constraint(equalTo: self.centerYAnchor),
redBox.widthAnchor.constraint(equalToConstant: 100),
redBox.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

UICollectionViewCell layout with SnapKit

I have a simple UICollectionViewCell which is full width and I'm using SnapKit to layout its subviews. I'm using SnapKit for my other view and it's working great but not within the collectionViewCell. This is the layout I'm trying to achieve:
collectionViewCell layout
The code for the collectionViewCell is:
lazy var imageView: UIImageView = {
let img = UIImageView(frame: .zero)
img.contentMode = UIViewContentMode.scaleAspectFit
img.backgroundColor = UIColor.lightGray
return img
}()
override init(frame: CGRect) {
super.init(frame: frame)
let imageWidth = CGFloat(frame.width * 0.80)
let imageHeight = CGFloat(imageWidth/1.77)
backgroundColor = UIColor.darkGray
imageView.frame.size.width = imageWidth
imageView.frame.size.height = imageHeight
contentView.addSubview(imageView)
imageView.snp.makeConstraints { (make) in
make.top.equalTo(20)
//make.right.equalTo(-20)
//make.right.equalTo(contentView).offset(-20)
//make.right.equalTo(contentView.snp.right).offset(-20)
//make.right.equalTo(contentView.snp.rightMargin).offset(-20)
//make.right.equalToSuperview().offset(-20)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Without applying any constraints the imageView is displayed top left in the cell but applying any constraints makes the image disappear. Inspecting the collectionViewCell in debug view shows the imageView and constraints but the 'Size and horizontal position are ambiguous for UIImageView'.
I have also tried setting contentView.translatesAutoresizingMaskIntoConstraints = false amongst other things but the same results.
I'm using all code and no storyboard UI or layouts.
Thanks for any help!
You can't use both auto layout (SnapKit) and manual layout (set frame). Usually, using auto layout will cause manual layout to fail. However, your code of the constraints in snp closure is not complete. Whether using auto layout or manual layout, finally the view should get the position and size.
So, you can try like this:
imageView.snp.makeConstraints { make in
// set size
make.size.equalTo(CGSize(width: imageWidth, height: imageHeight))
// set position
make.top.equalTo(20)
make.centerX.equalTo(contentView)
}
I don't know much about SnapKit but it looks like you're missing a constraint. You need height, width, x and y. Works nicely with IOS 9 constraints.
let imageView: UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFit
view.backgroundColor = .lightGray
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
self.addSubview(imageView)
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
imageView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
imageView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 2/3, constant: -10).isActive = true
imageView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1/2, constant: -10).isActive = true
}

Resources