I'm creating a simple player app. There is a circle, that shows a progress of playing a song.
What is the best way to draw this circle in Swift and make a mask? I assume I can draw a 2 circles putting the width stroke to the thickness I want and without filling it. And the white one has to be masked according to some parameter. I don't have an idea, how to mask it in a proper way.
I came up with this solution recently:
class CircularProgressView: UIView {
private let floatPi = CGFloat(M_PI)
private var progressColor = UIColor.greenColor()
private var progressBackgroundColor = UIColor.grayColor()
#IBInspectable var percent: CGFloat = 0.11 {
didSet {
setNeedsDisplay()
}
}
#IBInspectable var lineWidth: CGFloat = 18
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let origo = CGPointMake(frame.size.width / 2, frame.size.height / 2)
let radius: CGFloat = frame.size.height / 2 - lineWidth / 2
CGContextSetLineWidth(context, lineWidth)
CGContextMoveToPoint(context, frame.width / 2, lineWidth / 2)
CGContextAddArc(context, origo.x, origo.y, radius, floatPi * 3 / 2, floatPi * 3 / 2 + floatPi * 2 * percent, 0)
progressColor.setStroke()
let lastPoint = CGContextGetPathCurrentPoint(context)
CGContextStrokePath(context)
CGContextMoveToPoint(context, lastPoint.x, lastPoint.y)
CGContextAddArc(context, origo.x, origo.y, radius, floatPi * 3 / 2 + floatPi * 2 * percent, floatPi * 3 / 2, 0)
progressBackgroundColor.setStroke()
CGContextStrokePath(context)
}
}
You just have to set a correct frame to it (via code or interface builder), and set the percent property.
This solution is not using mask or two circles, just two arcs, the first start at 12 o clock and goes to 2 * Pi * progress percent, and the other arc is drawn from the end of the previous arc to 12 o clock.
Important: the percent property has to be between 0 and 1!
Related
How we can increase the size of the selected date title circle in FSCalendar libraryas we can see in the image I want the circle bigger than current size
calendar.appearance.borderRadius = .someValue // I tried all Value of enum But it not works
Override FSCalendarCell layoutSubviews method.
Here is my code.
override func layoutSubviews() {
super.layoutSubviews()
let titleHeight: CGFloat = self.bounds.size.height * 4.1 / 5
var diameter: CGFloat = min(self.bounds.size.height * 5.2 / 8, self.bounds.size.width)
diameter = diameter > FSCalendarStandardCellDiameter ? (diameter - (diameter-FSCalendarStandardCellDiameter) * 0.5) : diameter
shapeLayer.frame = CGRect(x: (bounds.size.width - diameter) / 2,
y: (titleHeight - diameter) / 2,
width: diameter, height: diameter)
let path = UIBezierPath(roundedRect: shapeLayer.bounds, cornerRadius: shapeLayer.bounds.width * 0.5 * appearance.borderRadius).cgPath
if shapeLayer.path != path {
shapeLayer.path = path
}
}
Change the value titleHeight, diameter to increase/decrease the size of selected date circle.
I'm building an app where I want to display profilePictures of people "near you" in a hexagon beehive style.
The full beehive should be draggable, like google maps for example.
My question is if this is something I can do with just using UIKit, or if it would be easier to use UIKit and SpriteKit together.
I hope someone could point me at the right direction and or have some ideas on how this could be made. Thank you for your time!
Update:
Just to make my question a bit more clear.
This is how my view looks like atm
And this is what I want to achieve
In the first image I´ve just set the X and Y pos of the UIImage center middle.
I want to create some sort of function that can get an array of different profilePics and then put the out in this pattern.
UIKit alone can do the job: you should try to setup a mask with CALayer on a UIImageView for instance.
The draggable behavior thing can be achieved either with a UIScrollView by adding and arranging all your image subviews in it, or with a UICollectionView with a custom flow, but it may be much harder to set up.
For the hexagon views, you'll find here an interesting example you can adapt for your usage: http://sapandiwakar.in/make-hexagonal-view-on-ios/
Here is an adaption of Sapan Diwakar solution in Swift 4.2 and using extensions:
extension UIBezierPath {
convenience init(roundedPolygonPathInRect rect: CGRect, lineWidth: CGFloat, sides: NSInteger, cornerRadius: CGFloat = 0, rotationOffset: CGFloat = 0) {
self.init()
let theta: CGFloat = 2.0 * CGFloat.pi / CGFloat(sides) // How much to turn at every corner
let width = min(rect.size.width, rect.size.height) // Width of the square
let center = CGPoint(x: rect.origin.x + width / 2.0, y: rect.origin.y + width / 2.0)
// Radius of the circle that encircles the polygon
// Notice that the radius is adjusted for the corners, that way the largest outer
// dimension of the resulting shape is always exactly the width - linewidth
let radius = (width - lineWidth + cornerRadius - (cos(theta) * cornerRadius)) / 2.0
// Start drawing at a point, which by default is at the right hand edge
// but can be offset
var angle = CGFloat(rotationOffset)
let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
move(to: CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta)))
for _ in 0 ..< sides {
angle += theta
let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
let tip = CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
let start = CGPoint(x: corner.x + cornerRadius * cos(angle - theta), y: corner.y + cornerRadius * sin(angle - theta))
let end = CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta))
addLine(to: start)
addQuadCurve(to: end, controlPoint: tip)
}
close()
}
}
extension UIImageView {
func setupHexagonMask(lineWidth: CGFloat, color: UIColor, cornerRadius: CGFloat) {
let path = UIBezierPath(roundedPolygonPathInRect: bounds, lineWidth: lineWidth, sides: 6, cornerRadius: cornerRadius, rotationOffset: CGFloat.pi / 2.0).cgPath
let mask = CAShapeLayer()
mask.path = path
mask.lineWidth = lineWidth
mask.strokeColor = UIColor.clear.cgColor
mask.fillColor = UIColor.white.cgColor
layer.mask = mask
let border = CAShapeLayer()
border.path = path
border.lineWidth = lineWidth
border.strokeColor = color.cgColor
border.fillColor = UIColor.clear.cgColor
layer.addSublayer(border)
}
}
And then you can just use it like that:
let image = UIImageView(frame: CGRect(x: 30, y: 30, width: 300, height: 300))
image.contentMode = .scaleAspectFill
image.image = UIImage(named: "lenna.png")
image.setupHexagonMask(lineWidth: 5.0, color: .white, cornerRadius: 20.0)
view.addSubview(image)
EDIT: As I told you, the easiest way is to use a UIScrollView to display your map, and with simple math you can display your hexagons the way you want.
Here is a small example you must adapt to match your requirements. For example you should be extra careful with performance. This example should not be used as is, if you have many images, you should load them on the fly and remove them when you don't show them. And you can even think using a background rendering if it takes too much fps...
Assuming view is a UIScrollView:
let hexaDiameter : CGFloat = 150
let hexaWidth = hexaDiameter * sqrt(3) * 0.5
let hexaWidthDelta = (hexaDiameter - hexaWidth) * 0.5
let hexaHeightDelta = hexaDiameter * 0.25
let spacing : CGFloat = 5
let rows = 10
let firstRowColumns = 6
view.contentSize = CGSize(width: spacing + CGFloat(firstRowColumns) * (hexaWidth + spacing),
height: spacing + CGFloat(rows) * (hexaDiameter - hexaHeightDelta + spacing) + hexaHeightDelta)
for y in 0..<rows {
let cellsInRow = y % 2 == 0 ? firstRowColumns : firstRowColumns - 1
let rowXDelta = y % 2 == 0 ? 0.0 : (hexaWidth + spacing) * 0.5
for x in 0..<cellsInRow {
let image = UIImageView(frame: CGRect(x: rowXDelta + CGFloat(x) * (hexaWidth + spacing) + spacing - hexaWidthDelta,
y: CGFloat(y) * (hexaDiameter - hexaHeightDelta + spacing) + spacing,
width: hexaDiameter,
height: hexaDiameter))
image.contentMode = .scaleAspectFill
image.image = UIImage(named: "lenna.png")
image.setupHexagonMask(lineWidth: 5.0, color: .white, cornerRadius: 10.0)
view.addSubview(image)
}
}
I'm developing an internet speed test app. Something I will do to practice and learn more about a future project.
This is my Swift Code:
import UIKit
#IBDesignable
class Arc: UIView {
#IBInspectable var dashWidth: CGFloat = 12.0
#IBInspectable var smallDashWidth: CGFloat = 5.0
override func draw(_ rect: CGRect) {
// Position and Radius of Arc
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
// Calculate Angles
let π = CGFloat(M_PI)
let startAngle = 3 * π / 4
let endAngle = π / 4
let radius = max(bounds.width / 1.15, bounds.height / 1.15) / 2 - dashWidth / 2
// Start Context
let context = UIGraphicsGetCurrentContext()
// MARK: Base
let arc = UIBezierPath(arcCenter: center, radius: radius,startAngle: startAngle,endAngle: endAngle,clockwise: true)
arc.addArc(withCenter: center, radius: radius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
arc.lineJoinStyle = .bevel
arc.lineCapStyle = .round
arc.close()
UIColor.yellow().setStroke()
arc.lineWidth = smallDashWidth
//context!.saveGState()
context!.setLineDash(phase: 0, lengths: [0, 0], count: 2)
arc.stroke()
context!.saveGState()
// MARK: dash Arc
let dashArc = UIBezierPath()
dashArc.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
// Round Line
dashArc.lineJoinStyle = .round;
// Set Stroke and Width of Dash
UIColor.white().setStroke()
dashArc.lineWidth = dashWidth
// Save Context and Set Line Dash
context!.saveGState()
context!.setLineDash(phase: 0, lengths: [2, 54], count: 2)
// Draw Line
dashArc.stroke()
// Restore Context
context!.restoreGState()
}
}
The result is this:
What I need to do:
I need to automate this line of code:
context!.setLineDash(phase: 0, lengths: [2, 54], count: 2)
The lengths are [2, 54] which numbers are added without calculation, only to get the final equation number taken to obtain this dynamically.
1: need to add dashes 12 (which may later be changed, being assigned as a variable) across the arc. The arc begins and ends at a variable angle (possibility to change later).
2: The value of dashArc.lineWidth = dashWidth can also be changed, and is an important item to calculate the space between the 12 dashes.
3: Since all the variables presented values can be variable, which is the best way to do this calculation.
4: The first and the last dash should be at the same angle that the respective startAngle and endAngle.
What I need:
I need a calculation that looks and spreads as symmetrically as possible the dashes during the arc.
I thought of a similar calculation to this:
var numberOfDashes = 12
var perimeterArc = ?
var widthDash = 2
spacing = (perimeterArc - (widthDash * numberOfDashes)) / numberOfDashes
context!.setLineDash(phase: 0, lengths: [widthDash, spacing], count: 2)
But I do not know how to calculate the perimeterArc.
Can someone help me? I could not think of anything to create a logical calculation for this in Swift 2/3.
I appreciate any tip.
Instead of trying to compute spaces directly by trying to use a dash pattern, first of all, think in terms of angles.
I lifted some code that was originally created as an example in my book PostScript By Example (page 281). I transliterated the postscript code to Swift (version 2, as version 3 can't seem to do anything useful).
I also eschewed your use of UIBezierPath mixed in with an access to the Graphics Context, since I have the feeling that there are some strange interactions between the two. UIBezierPath is intended to manage the Graphics Context under the covers.
Here is the code:
class ComputeDashes : UIView
{
let insetAmount: CGFloat = 40.0
let numberOfDashes: Int = 16
let startAngle: CGFloat = CGFloat(1.0 * M_PI / 4.0)
let endAngle: CGFloat = CGFloat(3.0 * M_PI / 4.0)
let subtendedAngle: CGFloat = CGFloat(2.0 * M_PI) - CGFloat(2.0 * M_PI / 4.0)
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
super.init(coder: coder)!
}
override func drawRect(rect: CGRect)
{
let insets: UIEdgeInsets = UIEdgeInsetsMake(insetAmount, insetAmount, insetAmount, insetAmount)
let newbounds: CGRect = UIEdgeInsetsInsetRect(self.bounds, insets)
let centre: CGPoint = CGPointMake(CGRectGetMidX(newbounds), CGRectGetMidY(newbounds))
let radius: CGFloat = CGRectGetWidth(newbounds) / 2.0
let context: CGContext = UIGraphicsGetCurrentContext()!
CGContextAddArc(context, centre.x, centre.y, radius, startAngle, endAngle, 1)
CGContextSetLineWidth(context, 10.0)
UIColor.magentaColor().set()
CGContextStrokePath(context)
// MARK: paint dashes
let innerRadius: CGFloat = radius * 0.75
CGContextSaveGState(context)
CGContextTranslateCTM(context, centre.x, centre.y)
let angle = subtendedAngle / CGFloat(numberOfDashes)
CGContextRotateCTM(context, endAngle)
for rot in 0...numberOfDashes {
let innerPoint: CGPoint = CGPointMake(innerRadius, 0.0)
CGContextMoveToPoint(context, innerPoint.x, innerPoint.y)
let outerPoint: CGPoint = CGPointMake(radius, 0.0)
CGContextAddLineToPoint(context, outerPoint.x, outerPoint.y)
CGContextRotateCTM(context, angle)
}
CGContextSetLineWidth(context, 2.0)
UIColor.blackColor().set()
CGContextStrokePath(context)
CGContextRestoreGState(context)
}
}
I believe that this approach is much more flexible, and avoids the tricky computations you would need to do to account for line widths in the 'on' phase of the dash pattern.
I hope this helps. Let me know what you think.
Am am writing some code to draw circular charts, and while reading some example code, became confused about how angles are measured in Swift.
In the first post I read, Swift seems to use radians:
CGContextAddArc(context, origo.x, origo.y, radius, floatPi * 3 / 2, floatPi * 3 / 2 + floatPi * 2 * percent, 0)
In the second post, Swift seems to define angles as a range from 0 to 1:
let circlePath = UIBezierPath(ovalInRect: CGRect(x: 200, y: 200, width: 150, height: 150))
var segments: [CAShapeLayer] = []
let segmentAngle: CGFloat = (360 * 0.125) / 360
for var i = 0; i < 8; i++ {
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.CGPath
// start angle is number of segments * the segment angle
circleLayer.strokeStart = segmentAngle * CGFloat(i)
// end angle is the start plus one segment, minus a little to make a gap
// you'll have to play with this value to get it to look right at the size you need
let gapSize: CGFloat = 0.008
circleLayer.strokeEnd = circleLayer.strokeStart + segmentAngle - gapSize
circleLayer.lineWidth = 10
circleLayer.strokeColor = UIColor(red:0, green:0.004, blue:0.549, alpha:1).CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
// add the segment to the segments array and to the view
segments.insert(circleLayer, atIndex: i)
view.layer.addSublayer(segments[i])
}
So, how does Swift measure angles?
Thanks
Swift has absolutely nothing to do with the angle unit, it's just a Float/Double to it. It's the functions of the Core Graphics framework that specify what angle unit they're expecting. Just read the docs of the functions in question.
From the CGContextAddArc docs:
startAngle: The angle to the starting point of the arc, measured in radians from the positive x-axis.
endAngle: The angle to the end point of the arc, measured in radians from the positive x-axis.
Also the strokeStart and strokeEnd properties of the CAShapeLayer say what part of the given path should be drawn, it's not an angle. Just read the docs next time.
I have a bunch of views in my app. I would like to arrange them in a circular shape and change their center depending on the number of views present.
So, if there are 3 views they would look like a triangle, but would still form a circle. If there are 4 it would look like a square but still form a circle, and so on...
In short, the centers of all views would sit on a imaginary circle.
Any suggestions?
This is the code I used in one of my projects, hope it helps.
// you must set both of these
CGPoint centerOfCircle;
float radius;
int count = 0;
float angleStep = 2.0f * M_PI / [arrayOfViews count];
for (UIView *view in arrayOfViews) {
float xPos = cosf(angleStep * count) * radius;
float yPos = sinf(angleStep * count) * radius;
view.center = CGPointMake(centerOfCircle.x + xPos, centerOfCircle.y +yPos);
count++;
}
Here's a Swift 3 version of the accepted answer, as an UIView extension with offset arguments:
public extension UIView {
public func distributeSubviewsInACircle(xOffset: CGFloat, yOffset: CGFloat) {
let center = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.size.height / 2)
let radius: CGFloat = self.bounds.size.width / 2
let angleStep: CGFloat = 2 * CGFloat(Double.pi) / CGFloat(self.subviews.count)
var count = 0
for subview in self.subviews {
let xPos = center.x + CGFloat(cosf(Float(angleStep) * Float(count))) * (radius - xOffset)
let yPos = center.y + CGFloat(sinf(Float(angleStep) * Float(count))) * (radius - yOffset)
subview.center = CGPoint(x: xPos, y: yPos)
count += 1
}
}
}
You could divide the degrees of a circle (360 degrees or 2π radians) by the number of views you have, then adjust their centers based on the angle and the distance from the centre.
Here are some functions I use:
// These calculate the x and y offset from the center by using the angle in radians
#define LengthDir_X(__Length__,__Direction__) (cos(__Direction__)*__Length__)
#define LengthDir_Y(__Length__,__Direction__) (sin(__Direction__)*__Length__)
// I use this to convert degrees to radians and back if I have to
#define DegToRad(__ANGLE__) (((__ANGLE__) * 2.0 * M_PI) / 360.0)
#define RadToDeg(__ANGLE__) (((__ANGLE__) * 360) / (2.0 * M_PI))