I'm pretty new to Swift and coding in general. I'm trying to just create 4 simple grey circles that are positioned within 4 UIViews that are inside 2 stackViews...etc
With the 4 UIViews, I have them basically in a grid square in the centre of the screen, I'll add a screenshot for clarity below. At the moment, it seems to render the bottom circles quite far below the boxes that I have laid out in the XCode IB.
I've tried a few different ways of trying to get the circles positioned properly, but I've hit a brick wall and can't seem to work out why it's not working. I can get them to position properly if I add a UIView within the UIViews (Named 'topLeftBox', 'topRightBox'...etc). And whilst it's a simple work around, I'm just trying to understand why I can't just add the layers in the original 4 UIViews.
Any help would be much appreciated, and apologies in advance for the glaring Newbie mistakes!
Heres a screenshot of the output as the code stands now and also the XCodeIB:
XCode Storyboard
Grey Circles Simulator Output
And here is the code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var topLeftBox: UIView!
#IBOutlet weak var topRightBox: UIView!
#IBOutlet weak var bottomLeftBox: UIView!
#IBOutlet weak var bottomRightBox: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
drawCircle(box: topLeftBox)
drawCircle(box: bottomLeftBox)
drawCircle(box: topRightBox)
drawCircle(box: bottomRightBox)
}
func createCircle(lineColor: UIColor, strokeEnd: CGFloat) -> CAShapeLayer {
let layer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: .zero, radius: 50, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
// Styling for the circular line
layer.path = circularPath.cgPath
layer.strokeColor = lineColor.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 10
layer.lineCap = kCALineCapRound
layer.strokeEnd = strokeEnd
layer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
return layer
// Style the track
}
fileprivate func drawCircle(box: UIView) {
// Create Circles
let trackLayer = createCircle(lineColor: .lightGray, strokeEnd: 1)
box.layer.addSublayer(trackLayer)
trackLayer.position = box.center
}
}
Your problem is this line:
trackLayer.position = box.center
That is using the center of the box's frame which is in the coordinate system of its parent view. You need the center of the box in the coordinate system of the box itself (ie. its bounds).
Replace the line above with this:
trackLayer.position = CGPoint(x: box.bounds.midX, y: box.bounds.midY)
Related
What I am trying to do is to get the position of my label (timerLabel) in order to pass those coordinates to UIBezierPath (so that the center of the shape and the center of the label coincide).
Here's my code so far, inside the viewDidLoad method, using Xcode 13.2.1:
// getting the center of the label
let center = CGPoint.init(x: timerLabel.frame.midX , y: timerLabel.frame.midY)
// drawing the shape
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
and this is what I have when I run my app:
link
What I don't understand is why I get (0,0) as coordinates even though I access the label's property (timerLabel.frame.midX).
The coordinates of your label may vary depending on current layout. You need to track all changes and reposition your circle when changes occur. In view controller that uses constraints you would override
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// recreate your circle here
}
this alone does not explain why your circle is so far out. First of all, looking at your image you do not get (0, 0) but some other value which may be relative position of your label within the blue bubble. The frame is always relative to its superview so you need to convert that into your own coordinate system:
let targetView = self.view!
let sourceView = timerLabel!
let centerOfSourceViewInTargetView: CGPoint = targetView.convert(CGPoint(x: sourceView.bounds.midX, y: sourceView.bounds.midY), to: targetView)
// Use centerOfSourceViewInTargetView as center
but I suggest using neither of the two. If you are using constraints (which you should) then rather create more views than adding layers to your existing views.
For instance you could try something like this:
#IBDesignable class CircleView: UIView {
#IBInspectable var lineWidth: CGFloat = 10 { didSet { refresh() } }
#IBInspectable var strokeColor: UIColor = .lightGray { didSet { refresh() } }
override var frame: CGRect { didSet { refresh() } }
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let fillRadius: CGFloat = min(bounds.width, bounds.height)*0.5
let strokeRadius: CGFloat = fillRadius - lineWidth*0.5
let path = UIBezierPath(ovalIn: .init(x: bounds.midX-strokeRadius, y: bounds.midY-strokeRadius, width: strokeRadius*2.0, height: strokeRadius*2.0))
path.lineWidth = lineWidth
strokeColor.setStroke()
UIColor.clear.setFill() // Probably not needed
path.stroke()
}
private func refresh() {
setNeedsDisplay() // This is to force redraw
}
}
this view should draw your circle within itself by overriding draw rect method. You can easily use it in your storyboard (first time it might not draw in storyboard because Xcode. Simply close your project and reopen it and you should see the circle even in storyboard).
Also in storyboard you can directly modify both line width and stroke color which is very convenient.
About the code:
Using #IBDesignable to see drawing in storyboard
Using #IBInspectable to be able to set values in storyboard
Refreshing on any value change to force redraw (sometimes needed)
When frame changes forcing a redraw (Needed when setting frame from code)
A method layoutSubviews is called when resized from constraints. Again redrawing.
Path is computed so that it fits within the size of view.
How to create this kind of UI?
So far, I end up creating the same UI like below.
I am not sure, how to curve that yellow colored border as in the above reference.
Simply adding a border layout and adding a mask you can achieve what you need
Full Example (only relevant code)
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var selectedIndex : Int = -1
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.tableView.layer.borderColor = UIColor.red.cgColor
self.tableView.layer.borderWidth = 3
}
func bezierPathWithShape(rect:CGRect,cornerRadius:CGFloat) ->UIBezierPath
{
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
return path
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let mask = CAShapeLayer(layer: self)
mask.path = self.bezierPathWithShape(rect: self.tableView.bounds, cornerRadius: 15).cgPath
self.tableView.layer.mask = mask
self.tableView.layer.masksToBounds = true
}
}
Result
Here is another way to do it.
Take a view which is superView to both the tableView and the lines.
Add cornerRadius, then set clipToBounds = true for the superView.
It will clip the lines according to the cornerRadius.
I am having three UIView's one after the other horizontally, with each UiView.Layer.BorderColor set to Black and UiView.Layer.BorderWidth set to 0.25f like shown below. So this gives a border look for each of the view.
Now i have a requirement to only display the horizontal borders of the UiViews. Hence i create a mask layer and set path and frame to that mask layer to apply clip. Please refer the code snippet below to give you an idea of what am doing.
foreach (UIView UiView in myViews)
{
UiView.Layer.BorderColor = Color.Black.ToCGColor();
UiView.Layer.BorderWidth = 0.25f;
UIBezierPath maskPath = null;
CAShapeLayer maskLayer = new CAShapeLayer();
maskLayer.Frame = UiView.Bounds;
// Applying Clip to my layer
maskPath = UIBezierPath.FromRect(new CGRect(UiView.Bounds.Left + 0.5, UiView.Bounds.Top, UiView.Bounds.Width - 1, UiView.Bounds.Height));
maskLayer.Path = maskPath.CGPath;
view.Layer.Mask = maskLayer;
}
Yes i am aware that whatever the bounds i set for the maskPath is the Visible region, so given that each UiView is 60 in width , my visible region according to what i have written in code is , from 0.5 pixel upto 59.5 pixel of each UIView . Thus eliminating the borders present in the pixels from 0 to 0.5 and from 59.5 to 60. Hope this part is clear for you. Please refer the below image for a visual representation of my mask area. The Yellow borders denotes the mask bounds.
Now all is fine , but this 0.5 pixels border which is hidden causes a white space in my top and bottom borders of the UiView's put together continuously. Please refer the below image with the circled spots highlighting the void spaces in the top and bottom.
This can be easily reproduced in code too. Please suggest me on how to overcome this problem. What am i doing wrong. I know this is a very small blank space of half a pixel which is barely visible to the naked eye most of the times, but it will not be visually pleasing for my grid view application.
PS : Am developing in Xamarin.iOS , however native iOS solutions will also be helpful.
Thanks in advance
Try this for Swift.
class ViewController: UIViewController {
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
#IBOutlet weak var view3: UIView!
override func viewDidLoad() {
super.viewDidLoad()
var borderwidth = CGFloat()
borderwidth = 5
let topBorder = CALayer()
topBorder.backgroundColor = UIColor.black.cgColor
topBorder.frame = CGRect(x: self.view1.frame.origin.x, y: self.view1.frame.origin.y, width: self.view3.frame.origin.x + self.view3.frame.size.width - self.view1.frame.origin.x, height: borderwidth)
self.view.layer.addSublayer(topBorder)
let bottomBorder = CALayer()
bottomBorder.backgroundColor = UIColor.black.cgColor
bottomBorder.frame = CGRect(x: self.view1.frame.origin.x, y: self.view1.frame.origin.y + self.view1.frame.size.height - borderwidth, width: self.view3.frame.origin.x + self.view3.frame.size.width - self.view1.frame.origin.x, height: borderwidth)
self.view.layer.addSublayer(bottomBorder)
}
}
See this screenshot : 1
Masking is the problem. Try like this.
view.Layer.MasksToBounds = false;
view.Layer.AllowsEdgeAntialiasing = true;
layer.BorderColor = color.ToCGColor();
layer.BorderWidth = 0.5f;
if (layer.SuperLayer == null && layer.SuperLayer != view.Layer)
view.Layer.AddSublayer(layer);
view.Layer.BorderColor = Color.Transparent.ToCGColor();
layer.Frame = new CGRect(view.Bounds.Left, view.Bounds.Bottom - 0.5f, view.Bounds.Right, view.Bounds.Bottom);
I created an extension on UIView so that I can create circle views easily without writing the code in each custom component. My code looks as:
extension UIView {
func createCircleView(targetView: UIView) {
let square = CGSize(width: min(targetView.frame.width, targetView.frame.height), height: min(targetView.frame.width, targetView.frame.height))
targetView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: square)
targetView.layer.cornerRadius = square.width / 2.0
}
}
the purpose of the property square is to always compute a perfect square based on the smallest property of width or height from the target view, this stops rectangles from trying to become squares, as that could obviously never produce a circle.
Inside my custom component I call this method with:
// machineCircle is a child view of my cell
#IBOutlet weak var machineCircle: UIView!
// Whenever data is set, update the cell with an observer
var machineData: MachineData? {
didSet {
createCircleView(machineCircle)
}
}
The problem I am having is that my circles are rendering to the screen like this:
When debugging, I inspected the square variable, it consistently prints width: 95, height: 95, which would lead me to believe that a perfect circle should be rendered each time.
Why am I seeing these strange shapes?
UPDATE I have found why perfect circles aren't being formed but I am not sure how to go about it.
In my storyboard I set the default size of my machineCircle view to be 95x95, however when my view loads, the collection cells width and height are computed dynamically with this method:
override func viewDidLoad() {
super.viewDidLoad()
let width = CGRectGetWidth(collectionView!.frame) / 3
let layout = collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = CGSize(width: width, height: width + width / 2)
}
This resizes the collection view cells so that they can fit in cols of 3 accross the screen, but it does not seem to change the base scale of the inner machineCircle view. The machineCircle view still retains its size of 95x95 but seems to scale down inside the view causing the effect to be caused (thats what I have observed thus far). Any ideas?
Taking Matt's advice, I created a method to draw a circle within a UIView using CALayers.
For any who are interested, here is my implementation:
func drawCircleInView(parentView: UIView, targetView: UIView, color: UIColor, diameter: CGFloat)
{
let square = CGSize(width: min(parentView.bounds.width, parentView.bounds.height), height: min(parentView.bounds.width, parentView.bounds.height))
let center = CGPointMake(square.width / 2 - diameter, square.height / 2 - diameter)
let circlePath = UIBezierPath(arcCenter: center, radius: CGFloat(diameter), startAngle: CGFloat(0), endAngle: CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
print(targetView.center)
shapeLayer.path = circlePath.CGPath
shapeLayer.fillColor = color.CGColor
shapeLayer.strokeColor = color.CGColor
shapeLayer.lineWidth = 1.0
targetView.backgroundColor = UIColor.clearColor()
targetView.layer.addSublayer(shapeLayer)
}
And it is called with:
drawCircleInView(self, machineCircle, color: UIColor.redColor(), radius: 30)
Here is the result:
The white box behind is for demonstration purposes only, it shows the parent view that the circle is drawn into, this will be set to transparent in production.
I have a universal iOS swift project in xcode, and am having trouble getting the sizes of a UIImageView right. The XIB structure is as follows:
View
|-- Card (View)
|-- CoverImage (UIImageView)
View is loaded into a UIScrollView by ViewController.swift. Card has top, bottom, trailing, and leading constraints set up so that there is a 20px 'margin' around it. Card also has rounded corners. CoverImage has similar constraints, with top, trailing, and leading being set to superview and bottom being defined in viewDidLoad() as 40% of the height of Card. The problem is that the width of CoverImage exceeds the width of Card, with the excess not being displayed. As a result, when I try to round the top corners of CoverImage, only the Top Left corner is rounded.
Setting leadingConstraint.constant or trailingConstraint.constant to 0 seems to have no effect, as the top right corner still remains unrounded.
How can I set CoverImage's constraints properly so that it has the correct size? Is it an issue with the constraints I'm setting in the XIB, or is there a better way to define the size of a child view?
For reference, this is the controller swift file for the View containing Card and CoverImage:
class CardViewController: UIViewController {
// MARK: Connect All Subviews/Objects
#IBOutlet weak var card: UIView!
#IBOutlet weak var coverImage: UIImageView!
#IBOutlet weak var bottomConstraint: NSLayoutConstraint!
#IBOutlet weak var topConstraint: NSLayoutConstraint!
#IBOutlet weak var trailingConstraint: NSLayoutConstraint!
#IBOutlet weak var leadingConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// MARK: Draw Card
card.layer.cornerRadius = 2
card.layer.shadowRadius = 3
card.layer.shadowOpacity = 0.15;
card.layer.shadowOffset = CGSizeMake(1, 1)
card.backgroundColor = UIColor.whiteColor()
// MARK: Draw Cover Image
let imageViewShape : CAShapeLayer = CAShapeLayer()
imageViewShape.bounds = coverImage.frame
imageViewShape.position = coverImage.center
imageViewShape.path = UIBezierPath(roundedRect: coverImage.bounds, byRoundingCorners: [UIRectCorner.TopRight, UIRectCorner.TopLeft], cornerRadii: CGSize(width: 2, height: 2)).CGPath
coverImage.layer.mask = imageViewShape
bottomConstraint.constant = card.frame.height * 0.4
}
}
Thanks to anyone who can help!
First of all, if you try to set the bounds of CAShapeLayer on viewDidLoad you'll get incorrect dimensions as for the superview isn't rendered on the device at the moment yet; it should be created on viewDidAppear.
Now that we have the correct frame we have to position it properly in superview and by default the mask we append does inherit the constraints of the view it's applied to, thus we have to adjust the origin point of it by 20 pixels on both axis.
The code I used to make it all look fine as you requested it to:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
let imageViewShape : CAShapeLayer = CAShapeLayer()
imageViewShape.frame = coverImage.frame
imageViewShape.bounds = CGRect(origin: CGPoint(x: coverImage.bounds.origin.x + 20, y: coverImage.bounds.origin.y + 20), size: coverImage.bounds.size)
imageViewShape.path = UIBezierPath(roundedRect: coverImage.bounds, byRoundingCorners: [UIRectCorner.TopRight, UIRectCorner.TopLeft, UIRectCorner.BottomLeft, UIRectCorner.BottomRight], cornerRadii: CGSize(width: 10, height: 10)).CGPath
coverImage.layer.mask = imageViewShape
}