iPad Pro Simulator not displaying layer.mask - swift

I have tested the following code on all simulators and it works fine except on the iPad Pro. On the the iPad Pro is does not display. The gradient layer will work fine, it is only when I try and apply a mask that it will not appear on the iPad Pro Simulator:
func createOverlay()
{
if !(gradientLayer != nil)
{
self.gradientLayer = CAGradientLayer()
self.layer.addSublayer(gradientLayer)
}
gradientLayer.frame = self.bounds
print(gradientLayer.frame)
gradientLayer.colors = [appColour.CGColor, appColourDark.CGColor]
//--------FROM HERE ON DOES NOT WORK ON IPAD PRO, NO CRASH BUT LAYER DOES NOT APPEAR
self.alpha = maskAlpha
let maskLayer = CAShapeLayer()
let path = CGPathCreateMutable()
let rect: CGRect = CGRect(x: xOffset - offset, y: yOffset - offset, width: circleWidth + (offset * 2), height: circleHeight + (offset * 2))
let bPath = UIBezierPath(ovalInRect: rect)
CGPathAddRect(path, nil, CGRectMake(0, 0, self.frame.width, self.frame.height))
CGPathAddPath(path, nil, bPath.CGPath)
maskLayer.backgroundColor = UIColor.blackColor().CGColor
maskLayer.path = path
maskLayer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = maskLayer
self.clipsToBounds = true
}
I am hoping this is just a simulator issue but if you see something in my code that might be causing a problem please let me know.
I have tried replacing the gradient layer with a normal layer but it still does not display.
Here is full code, it is a custom sub-class of UIView and is the top layer of a view controller setup in IB:
import UIKit
protocol TipSpeechDelegate
{
func stopSpeaking()
}
#IBDesignable
class HoleMaskView: UIView
{
var xOffset : CGFloat = 0.0
var yOffset : CGFloat = 0.0
var circleWidth: CGFloat = 0.0
var circleHeight: CGFloat = 0.0
var maskAlpha: CGFloat = 0.9
var offset: CGFloat = 10.0
var inset: CGFloat = 8.0
var tipText: String = ""
var myLabel: UILabel?
var gradientLayer: CAGradientLayer!
var relativeCorner: RelativeCornerType = RelativeCornerType.upperLeftCorner
var delegate: TipSpeechDelegate!
override func layoutSubviews()
{
super.layoutSubviews()
}
override func drawRect(rect: CGRect)
{
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(HoleMaskView.handleTap(_:)))
self.addGestureRecognizer(gestureRecognizer)
}
func drawTipText()
{
createOverlay()
let height: CGFloat = self.frame.size.height
if (self.myLabel != nil)
{
myLabel?.removeFromSuperview()
}
if (yOffset < height/2)
{
self.myLabel = UILabel(frame: CGRect(x: inset, y: (yOffset + circleHeight), width: self.frame.width-(inset*2), height: self.frame.height-(yOffset + circleHeight)))
}
else
{
self.myLabel = UILabel(frame: CGRect(x: inset, y: 0, width: self.frame.width-(inset*2), height: yOffset))
}
self.myLabel?.text = tipText
self.myLabel?.textColor = UIColor.whiteColor()
self.myLabel?.font = UIFont(name: "Avenir-Medium", size: 20.0)
self.myLabel?.textAlignment = .Center
self.myLabel?.lineBreakMode = .ByWordWrapping
self.myLabel?.numberOfLines = 0
self.myLabel?.setNeedsLayout()
self.addSubview(myLabel!)
}
func updateTipText(text: String, circle: CGRect)
{
self.tipText = text
yOffset = circle.origin.y
xOffset = circle.origin.x
circleWidth = circle.size.width
circleHeight = circle.size.height
self.drawTipText()
}
func tipText(text: String, rFrame: CGRect, inView: UIView) -> Bool
{
showTipMask()
let convertedPoint = inView.convertPoint(rFrame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = rFrame.size.width
circleHeight = rFrame.size.height
self.drawTipText()
return true
}
func tipText(text: String, button: UIButton) -> Bool
{
if button.hidden
{
return false
}
showTipMask()
let convertedPoint = button.superview!.convertPoint(button.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = button.frame.size.width
circleHeight = button.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String, label: UILabel) -> Bool
{
if label.hidden
{
return false
}
showTipMask()
let convertedPoint = label.superview!.convertPoint(label.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = label.frame.size.width
circleHeight = label.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String, textView: UITextView) -> Bool
{
if textView.hidden
{
return false
}
showTipMask()
let convertedPoint = textView.superview!.convertPoint(textView.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = textView.frame.size.width
circleHeight = textView.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String) -> Bool
{
showTipMask()
self.tipText = text
yOffset = 0.0
xOffset = self.frame.size.width/2
circleWidth = 0.0
circleHeight = 0.0
self.drawTipText()
return true
}
func tipText(text: String, view: UIView) -> Bool
{
if view.hidden
{
return false
}
showTipMask()
let convertedPoint = view.superview!.convertPoint(view.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = view.frame.size.width
circleHeight = view.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String, stepper: UIStepper) -> Bool
{
if stepper.hidden
{
return false
}
showTipMask()
let convertedPoint = stepper.superview!.convertPoint(stepper.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = stepper.frame.size.width
circleHeight = stepper.frame.size.height
self.drawTipText()
return true
}
func showTipMask()
{
self.alpha = alphaHide
self.hidden = false
UIView.animateWithDuration(0.5, animations:
{
self.alpha = alphaShow
}
)
}
func handleTap(gestureRecognizer: UIGestureRecognizer)
{
if delegate != nil
{
delegate.stopSpeaking()
}
print("tapped internal")
UIView.animateWithDuration(0.25, delay: 0.0, options: UIViewAnimationOptions.TransitionNone, animations:
{
() -> Void in
self.alpha = alphaHide
},
completion:
{
(finished: Bool) -> Void in
self.hidden = true
}
)
}
func createOverlay()
{
if !(gradientLayer != nil)
{
self.gradientLayer = CAGradientLayer()
self.layer.addSublayer(gradientLayer)
}
gradientLayer.frame = self.bounds
print(gradientLayer.frame)
gradientLayer.colors = [appColour.CGColor, appColourDark.CGColor]
self.alpha = maskAlpha
let maskLayer = CAShapeLayer()
let path = CGPathCreateMutable()
let rect: CGRect = CGRect(x: xOffset - offset, y: yOffset - offset, width: circleWidth + (offset * 2), height: circleHeight + (offset * 2))
let bPath = UIBezierPath(ovalInRect: rect)
CGPathAddRect(path, nil, CGRectMake(0, 0, self.frame.width, self.frame.height))
CGPathAddPath(path, nil, bPath.CGPath)
maskLayer.backgroundColor = UIColor.blackColor().CGColor
maskLayer.path = path
maskLayer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = maskLayer
self.clipsToBounds = true
}
}
Thanks
Greg

I am facing the same issue.
Currently I
turn on the Debug -> Optimize Rendering for Window Scale
scale it down to 50%
then the graph appears correctly.
But one of my colleges told me that he has to turn off the ORWS option. Wired.

Related

Cropping is not working perfectly as per the frame drawn

I am trying to crop a selected portion of NSImage which is fitted as per ProportionallyUpOrDown(AspectFill) Mode.
I am drawing a frame using mouse dragged event like this:
class CropImageView: NSImageView {
var startPoint: NSPoint!
var shapeLayer: CAShapeLayer!
var flagCheck = false
var finalPoint: NSPoint!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
override var image: NSImage? {
set {
self.layer = CALayer()
self.layer?.contentsGravity = kCAGravityResizeAspectFill
self.layer?.contents = newValue
self.wantsLayer = true
super.image = newValue
}
get {
return super.image
}
}
override func mouseDown(with event: NSEvent) {
self.startPoint = self.convert(event.locationInWindow, from: nil)
if self.shapeLayer != nil {
self.shapeLayer.removeFromSuperlayer()
self.shapeLayer = nil
}
self.flagCheck = true
var pixelColor: NSColor = NSReadPixel(startPoint) ?? NSColor()
shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 1.0
shapeLayer.fillColor = NSColor.clear.cgColor
if pixelColor == NSColor.black {
pixelColor = NSColor.color_white
} else {
pixelColor = NSColor.black
}
shapeLayer.strokeColor = pixelColor.cgColor
shapeLayer.lineDashPattern = [1]
self.layer?.addSublayer(shapeLayer)
var dashAnimation = CABasicAnimation()
dashAnimation = CABasicAnimation(keyPath: "lineDashPhase")
dashAnimation.duration = 0.75
dashAnimation.fromValue = 0.0
dashAnimation.toValue = 15.0
dashAnimation.repeatCount = 0.0
shapeLayer.add(dashAnimation, forKey: "linePhase")
}
override func mouseDragged(with event: NSEvent) {
let point: NSPoint = self.convert(event.locationInWindow, from: nil)
var newPoint: CGPoint = self.startPoint
let xDiff = point.x - self.startPoint.x
let yDiff = point.y - self.startPoint.y
let dist = min(abs(xDiff), abs(yDiff))
newPoint.x += xDiff > 0 ? dist : -dist
newPoint.y += yDiff > 0 ? dist : -dist
let path = CGMutablePath()
path.move(to: self.startPoint)
path.addLine(to: NSPoint(x: self.startPoint.x, y: newPoint.y))
path.addLine(to: newPoint)
path.addLine(to: NSPoint(x: newPoint.x, y: self.startPoint.y))
path.closeSubpath()
self.shapeLayer.path = path
}
override func mouseUp(with event: NSEvent) {
self.finalPoint = self.convert(event.locationInWindow, from: nil)
}
}
and selected this area as shown in picture using black dotted line:
My Cropping Code logic is this:
// resize Image Methods
extension CropProfileView {
func resizeImage(image: NSImage) -> Data {
var scalingFactor: CGFloat = 0.0
if image.size.width >= image.size.height {
scalingFactor = image.size.width/cropImgView.size.width
} else {
scalingFactor = image.size.height/cropImgView.size.height
}
let width = (self.cropImgView.finalPoint.x - self.cropImgView.startPoint.x) * scalingFactor
let height = (self.cropImgView.startPoint.y - self.cropImgView.finalPoint.y) * scalingFactor
let xPos = ((image.size.width/2) - (cropImgView.bounds.midX - self.cropImgView.startPoint.x) * scalingFactor)
let yPos = ((image.size.height/2) - (cropImgView.bounds.midY - (cropImgView.size.height - self.cropImgView.startPoint.y)) * scalingFactor)
var croppedRect: NSRect = NSRect(x: xPos, y: yPos, width: width, height: height)
let imageRef = image.cgImage(forProposedRect: &croppedRect, context: nil, hints: nil)
guard let croppedImage = imageRef?.cropping(to: croppedRect) else {return Data()}
let imageWithNewSize = NSImage(cgImage: croppedImage, size: NSSize(width: width, height: height))
guard let data = imageWithNewSize.tiffRepresentation,
let rep = NSBitmapImageRep(data: data),
let imgData = rep.representation(using: .png, properties: [.compressionFactor: NSNumber(floatLiteral: 0.25)]) else {
return imageWithNewSize.tiffRepresentation ?? Data()
}
return imgData
}
}
With this cropping logic i am getting this output:
I think as image is AspectFill thats why its not getting cropped in perfect size as per selected frame. Here if you look at output: xpositon & width & heights are not perfect. Or probably i am not calculating these co-ordinates properly. Let me know the faults probably i am calculating someting wrong.
Note: the CropImageView class in the question is a subclass of NSImageView but the view is layer-hosting and the image is drawn by the layer, not by NSImageView. imageScaling is not used.
When deciding which scaling factor to use you have to take the size of the image view into account. If the image size is width:120, height:100 and the image view size is width:120, height 80 then image.size.width >= image.size.height is true and image.size.width/cropImgView.size.width is 1 but the image is scaled because image.size.height/cropImgView.size.height is 1.25. Calculate the horizontal and vertical scaling factors and use the largest.
See How to crop a UIImageView to a new UIImage in 'aspect fill' mode?
Here's the calculation of croppedRect assuming cropImgView.size returns self.layer!.bounds.size.
var scalingWidthFactor: CGFloat = image.size.width/cropImgView.size.width
var scalingHeightFactor: CGFloat = image.size.height/cropImgView.size.height
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
switch cropImgView.layer?.contentsGravity {
case CALayerContentsGravity.resize: break
case CALayerContentsGravity.resizeAspect:
if scalingWidthFactor > scalingHeightFactor {
scalingHeightFactor = scalingWidthFactor
yOffset = (cropImgView.size.height - (image.size.height / scalingHeightFactor)) / 2
}
else {
scalingWidthFactor = scalingHeightFactor
xOffset = (cropImgView.size.width - (image.size.width / scalingWidthFactor)) / 2
}
case CALayerContentsGravity.resizeAspectFill:
if scalingWidthFactor < scalingHeightFactor {
scalingHeightFactor = scalingWidthFactor
yOffset = (cropImgView.size.height - (image.size.height / scalingHeightFactor)) / 2
}
else {
scalingWidthFactor = scalingHeightFactor
xOffset = (cropImgView.size.width - (image.size.width / scalingWidthFactor)) / 2
}
default:
print("contentsGravity \(String(describing: cropImgView.layer?.contentsGravity)) is not supported")
return nil
}
let width = (self.cropImgView.finalPoint.x - self.cropImgView.startPoint.x) * scalingWidthFactor
let height = (self.cropImgView.startPoint.y - self.cropImgView.finalPoint.y) * scalingHeightFactor
let xPos = (self.cropImgView.startPoint.x - xOffset) * scalingWidthFactor
let yPos = (cropImgView.size.height - self.cropImgView.startPoint.y - yOffset) * scalingHeightFactor
var croppedRect: NSRect = NSRect(x: xPos, y: yPos, width: width, height: height)
Bugfix: cropImgView.finalPoint should be the corner of the selection, not the location of mouseUp. In CropImageView set self.finalPoint = newPoint in mouseDragged instead of mouseUp.

How to update UIBezierpath and CAShapeLayer path with pan gesture of UIView?

'''
import UIKit
class CanvasView: UIView {
var circleViewTag = 1000
var coordinatePoints: [String] = ["243,103","534,86","243,286","426,286"] {
didSet {
self.updateCoordinateArray()
self.drawPoints()
}
}
fileprivate var coordArray: [CGPoint] = []
var shape = CAShapeLayer()
var path = UIBezierPath()
/*// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}*/
private func drawPoints() -> Void {
CommonMethods.printLog("\(coordinatePoints)")
self.layer.addSublayer(shape)
shape.opacity = 0.5
shape.lineWidth = 2
shape.lineJoin = CAShapeLayerLineJoin.miter
shape.strokeColor = UIColor.white.cgColor
shape.fillColor = UIColor(red: 1.0, green: 0.2, blue: 0.2, alpha: 0.2).cgColor
if let firstCoord = self.coordArray.first {
path.move(to: firstCoord)
}
for (index, cgPoint) in self.coordArray.enumerated() {
self.drawCircularPoint(points: cgPoint)
if index == 0 {
continue
}
path.addLine(to: cgPoint)
}
path.close()
shape.path = path.cgPath
//self.drawLineBetweenPoints()
}
private func drawCircularPoint(points: CGPoint) -> Void {
let circleView = UIView.init(frame: .zero)
circleViewTag = circleViewTag + 1
circleView.tag = circleViewTag
circleView.frame.size = CGSize.init(width: 30.0, height: 30.0)
circleView.layer.cornerRadius = 15.0
circleView.center = points
circleView.backgroundColor = .random()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.draggedView(_:)))
panGesture.view?.tag = circleView.tag
circleView.isUserInteractionEnabled = true
circleView.addGestureRecognizer(panGesture)
self.addSubview(circleView)
}
#objc func draggedView(_ sender:UIPanGestureRecognizer){
guard let getTag = sender.view?.tag else { return }
if let viewToDrag = self.viewWithTag(getTag) as? UIView {
var currentPoint: CGPoint = .zero
if path.contains(viewToDrag.center) {
print("YES")
currentPoint = path.currentPoint
}
let translation = sender.translation(in: self)
viewToDrag.center = CGPoint(x: viewToDrag.center.x + translation.x, y: viewToDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self)
if sender.state == .began && currentPoint != .zero {
let coordinateIndex = self.coordArray.firstIndex { (cgpoint) -> Bool in
if currentPoint == cgpoint {
return true
}
return false
}
if coordinateIndex != nil {
self.coordArray[coordinateIndex!] = viewToDrag.center
self.shape.removeFromSuperlayer()
self.path.removeAllPoints()
self.setNeedsDisplay()
self.layer.addSublayer(self.shape)
self.shape.opacity = 0.5
self.shape.lineWidth = 2
self.shape.lineJoin = CAShapeLayerLineJoin.miter
self.shape.strokeColor = UIColor.white.cgColor
self.shape.fillColor = UIColor(red: 1.0, green: 0.2, blue: 0.2, alpha: 0.2).cgColor
if let firstCoord = self.coordArray.first {
path.move(to: firstCoord)
}
for (index, cgPoint) in self.coordArray.enumerated() {
//self.drawCircularPoint(points: cgPoint)
if index == 0 {
continue
}
path.addLine(to: cgPoint)
}
path.close()
shape.path = path.cgPath
}
}
}
//self.bringSubviewToFront(viewDrag)
}
private func updateCoordinateArray() -> Void {
for singleCoordinate in self.coordinatePoints {
if singleCoordinate.contains(",") == true {
let splitCoordinate = singleCoordinate.split(separator: ",")
if splitCoordinate.count == 2 {
let xPos = CGFloat(Float(splitCoordinate[0]) ?? 0.0)
let yPos = CGFloat(Float(splitCoordinate[1]) ?? 0.0)
let cgPoint = CGPoint(x: xPos, y: yPos)
self.coordArray.append(cgPoint)
}
}
}
var penultimateIndex: Int?
if let penultimateCoordinate = self.coordArray.penultimate() {
penultimateIndex = self.coordArray.firstIndex { (cgpoint) -> Bool in
if penultimateCoordinate == cgpoint {
return true
}
return false
}
}
var lastIndex: Int?
if let lastCoordinate = self.coordArray.last {
lastIndex = self.coordArray.firstIndex { (cgpoint) -> Bool in
if lastCoordinate == cgpoint {
return true
}
return false
}
}
if penultimateIndex != nil && lastIndex != nil {
self.coordArray.swapAt(penultimateIndex!, lastIndex!)
}
}
'''
I am creating a polygon using UIBezierpath and CAShapelayer. Added pan gesture on all 4 points that is UIView. When I drag the point A,B,C,D the expected behaviour is that bezierpath and CAShapelayer gets updated with the updated points. And when user drag the inner part of the shape all the path gets updated. But I am unable to update the path and shape. Can anyone help me with this?
yon set name for layer
var shape = CAShapeLayer()
shape.name = "name1"
and then, instead of updating, you can delete it by first searching by name, and then add

Implement Like what's app story progress circle

I'm trying to do the below progress
Using MKMagneticProgress, I was able to apply the bottom space, but now I'm stuck on how to do the spaces in the circle, for example in this image we have the progress of 7 items out of 10.
What I want is to update the MKMagneticProgress code to add the spaces.
the code taken from MKMagneticProgress with my update (I added the total)
import UIKit
// MARK: - Line Cap Enum
public enum LineCap : Int{
case round, butt, square
public func style() -> String {
switch self {
case .round:
return CAShapeLayerLineCap.round.rawValue
case .butt:
return CAShapeLayerLineCap.butt.rawValue
case .square:
return CAShapeLayerLineCap.square.rawValue
}
}
}
// MARK: - Orientation Enum
public enum Orientation: Int {
case left, top, right, bottom
}
#IBDesignable
open class CircleProgress: UIView {
// MARK: - Variables
private let titleLabelWidth:CGFloat = 100
private let percentLabel = UILabel(frame: .zero)
#IBInspectable open var titleLabel = UILabel(frame: .zero)
/// Stroke background color
#IBInspectable open var clockwise: Bool = true {
didSet {
layoutSubviews()
}
}
/// Stroke background color
#IBInspectable open var backgroundShapeColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
updateShapes()
}
}
/// Progress stroke color
#IBInspectable open var progressShapeColor: UIColor = .blue {
didSet {
updateShapes()
}
}
/// Line width
#IBInspectable open var lineWidth: CGFloat = 8.0 {
didSet {
updateShapes()
}
}
/// Space value
#IBInspectable open var spaceDegree: CGFloat = 45.0 {
didSet {
// if spaceDegree < 45.0{
// spaceDegree = 45.0
// }
//
// if spaceDegree > 135.0{
// spaceDegree = 135.0
// }
layoutSubviews()
updateShapes()
}
}
/// The progress shapes line width will be the `line width` minus the `inset`.
#IBInspectable open var inset: CGFloat = 0.0 {
didSet {
updateShapes()
}
}
// The progress percentage label(center label) format
#IBInspectable open var percentLabelFormat: String = "%.f %%" {
didSet {
percentLabel.text = String(format: percentLabelFormat, progress * 100)
}
}
#IBInspectable open var percentColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
percentLabel.textColor = percentColor
}
}
/// progress text (progress bottom label)
#IBInspectable open var title: String = "" {
didSet {
titleLabel.text = title
}
}
#IBInspectable open var titleColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
titleLabel.textColor = titleColor
}
}
// progress text (progress bottom label)
#IBInspectable open var font: UIFont = .systemFont(ofSize: 13) {
didSet {
titleLabel.font = font
percentLabel.font = font
}
}
// progress Orientation
open var orientation: Orientation = .bottom {
didSet {
updateShapes()
}
}
/// Progress shapes line cap.
open var lineCap: LineCap = .round {
didSet {
updateShapes()
}
}
/// Returns the total Items
private var _total: CGFloat = 1.0
#IBInspectable open var total: CGFloat {
set {
self._total = newValue
self.setProgress(progress: self.progress)
}
get {
return self._total
}
}
/// Returns the current progress.
#IBInspectable open private(set) var progress: CGFloat {
set {
progressShape?.strokeEnd = newValue
}
get {
return progressShape.strokeEnd
}
}
/// Duration for a complete animation from 0.0 to 1.0.
open var completeDuration: Double = 2.0
private var backgroundShape: CAShapeLayer!
private var progressShape: CAShapeLayer!
private var progressAnimation: CABasicAnimation!
// MARK: - Init
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
backgroundShape = CAShapeLayer()
backgroundShape.fillColor = nil
backgroundShape.strokeColor = backgroundShapeColor.cgColor
layer.addSublayer(backgroundShape)
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = 0.0
progressShape.strokeEnd = 0.1
layer.addSublayer(progressShape)
progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
percentLabel.frame = self.bounds
percentLabel.textAlignment = .center
// percentLabel.textColor = self.progressShapeColor
self.addSubview(percentLabel)
percentLabel.text = String(format: "%.1f%%", progress * 100)
titleLabel.frame = CGRect(x: (self.bounds.size.width-titleLabelWidth)/2, y: self.bounds.size.height-21, width: titleLabelWidth, height: 21)
titleLabel.textAlignment = .center
// titleLabel.textColor = self.progressShapeColor
titleLabel.text = title
titleLabel.contentScaleFactor = 0.3
// textLabel.adjustFontSizeToFit()
titleLabel.numberOfLines = 2
//textLabel.adjustFontSizeToFit()
titleLabel.adjustsFontSizeToFitWidth = true
self.addSubview(titleLabel)
}
// MARK: - Progress Animation
public func setProgress(progress: CGFloat, animated: Bool = true) {
let actualProgress = progress/self._total
if actualProgress > 1.0 {
return
}
var start = progressShape.strokeEnd
if let presentationLayer = progressShape.presentation(){
if let count = progressShape.animationKeys()?.count, count > 0 {
start = presentationLayer.strokeEnd
}
}
let duration = abs(Double(progress - start)) * completeDuration
percentLabel.text = String(format: percentLabelFormat, actualProgress * 100)
progressShape.strokeEnd = actualProgress
if animated {
progressAnimation.fromValue = start
progressAnimation.toValue = actualProgress
progressAnimation.duration = duration
progressShape.add(progressAnimation, forKey: progressAnimation.keyPath)
}
}
// MARK: - Layout
open override func layoutSubviews() {
super.layoutSubviews()
backgroundShape.frame = bounds
progressShape.frame = bounds
let rect = rectForShape()
backgroundShape.path = pathForShape(rect: rect).cgPath
progressShape.path = pathForShape(rect: rect).cgPath
self.titleLabel.frame = CGRect(x: (self.bounds.size.width - titleLabelWidth)/2, y: self.bounds.size.height-50, width: titleLabelWidth, height: 42)
updateShapes()
percentLabel.frame = self.bounds
}
private func updateShapes() {
backgroundShape?.lineWidth = lineWidth
backgroundShape?.strokeColor = backgroundShapeColor.cgColor
backgroundShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
progressShape?.strokeColor = progressShapeColor.cgColor
progressShape?.lineWidth = lineWidth - inset
progressShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
switch orientation {
case .left:
titleLabel.isHidden = true
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi / 2, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1.0)
case .right:
titleLabel.isHidden = true
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi * 1.5, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 1.5, 0, 0, 1.0)
case .bottom:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: temp.bounds.size.height-50, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi * 2, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 2, 0, 0, 1.0)
case .top:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: 0, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 0, 1.0)
}
}
// MARK: - Helper
private func rectForShape() -> CGRect {
return bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
}
private func pathForShape(rect: CGRect) -> UIBezierPath {
let startAngle:CGFloat!
let endAngle:CGFloat!
if clockwise{
startAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
endAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
}else{
startAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
endAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
}
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.width / 2.0, startAngle: startAngle, endAngle: endAngle
, clockwise: clockwise)
return path
}
}
My output:
Thanks.
I was able to figure out how to update the code to add multiple arcs here is the updated code
import UIKit
// MARK: - Line Cap Enum
public enum LineCap : Int{
case round, butt, square
public func style() -> String {
switch self {
case .round:
return CAShapeLayerLineCap.round.rawValue
case .butt:
return CAShapeLayerLineCap.butt.rawValue
case .square:
return CAShapeLayerLineCap.square.rawValue
}
}
}
// MARK: - Orientation Enum
public enum Orientation: Int {
case left, top, right, bottom
}
#IBDesignable
open class CircleProgress: UIView {
// MARK: - Variables
private let titleLabelWidth:CGFloat = 100
private let percentLabel = UILabel(frame: .zero)
#IBInspectable open var titleLabel = UILabel(frame: .zero)
/// Stroke background color
#IBInspectable open var clockwise: Bool = true {
didSet {
layoutSubviews()
}
}
/// Stroke background color
#IBInspectable open var backgroundShapeColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
updateShapes()
}
}
/// Progress stroke color
#IBInspectable open var progressShapeColor: UIColor = .blue {
didSet {
updateShapes()
}
}
/// Line width
#IBInspectable open var lineWidth: CGFloat = 5.0 {
didSet {
updateShapes()
}
}
/// Space value
#IBInspectable open var spaceDegree: CGFloat = 45.0 {
didSet {
// if spaceDegree < 45.0{
// spaceDegree = 45.0
// }
//
// if spaceDegree > 135.0{
// spaceDegree = 135.0
// }
layoutSubviews()
updateShapes()
}
}
/// The progress shapes line width will be the `line width` minus the `inset`.
#IBInspectable open var inset: CGFloat = 0.0 {
didSet {
updateShapes()
}
}
// The progress percentage label(center label) format
#IBInspectable open var percentLabelFormat: String = "%.f %%" {
didSet {
percentLabel.text = String(format: percentLabelFormat, progress * 100)
}
}
#IBInspectable open var percentColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
percentLabel.textColor = percentColor
}
}
/// progress text (progress bottom label)
#IBInspectable open var title: String = "" {
didSet {
titleLabel.text = title
}
}
#IBInspectable open var titleColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
titleLabel.textColor = titleColor
}
}
// progress text (progress bottom label)
#IBInspectable open var font: UIFont = .systemFont(ofSize: 13) {
didSet {
titleLabel.font = font
percentLabel.font = font
}
}
// progress Orientation
open var orientation: Orientation = .bottom {
didSet {
updateShapes()
}
}
/// Progress shapes line cap.
open var lineCap: LineCap = .round {
didSet {
updateShapes()
}
}
/// Returns the total Items
private var _total: Int = 1
#IBInspectable open var total: Int {
set {
self._total = newValue
self.addProgressShapes()
}
get {
return self._total
}
}
/// Returns the current progress.
private var _progress: CGFloat = 0.0
#IBInspectable open private(set) var progress: CGFloat {
set {
self._progress = newValue
}
get {
return self._progress
}
}
/// Duration for a complete animation from 0.0 to 1.0.
open var completeDuration: Double = 1.0
private var backgroundShape: CAShapeLayer!
private var progressShapes: [CAShapeLayer]!
private var progressAnimation: CABasicAnimation!
// MARK: - Init
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
backgroundShape = CAShapeLayer()
backgroundShape.fillColor = nil
backgroundShape.strokeColor = backgroundShapeColor.cgColor
layer.addSublayer(backgroundShape)
progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
percentLabel.frame = self.bounds
percentLabel.textAlignment = .center
// percentLabel.textColor = self.progressShapeColor
self.addSubview(percentLabel)
percentLabel.text = String(format: "%.1f%%", progress * 100)
titleLabel.frame = CGRect(x: (self.bounds.size.width-titleLabelWidth)/2, y: self.bounds.size.height-21, width: titleLabelWidth, height: 21)
titleLabel.textAlignment = .center
// titleLabel.textColor = self.progressShapeColor
titleLabel.text = title
titleLabel.contentScaleFactor = 0.3
// textLabel.adjustFontSizeToFit()
titleLabel.numberOfLines = 2
//textLabel.adjustFontSizeToFit()
titleLabel.adjustsFontSizeToFitWidth = true
self.addSubview(titleLabel)
}
// MARK: - Progress Animation
private func addProgressShapes(){
self.progressShapes = []
let progressSize = 1.0
var size:CGFloat = CGFloat(progressSize / Double(self.total))
let padingPercent = 0.2
let pading: Double = padingPercent * Double(self.total)/10 * Double(progressSize / Double(self.total - 1))
size = size - CGFloat(pading)
print("")
print("size: \(size) | pading: \(pading)")
print("------------------------")
var start: CGFloat = 0.0
var end: CGFloat = size
print("start: \(start) | end: \(end) ")
print("------------------------")
let progressShape: CAShapeLayer!
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = start
progressShape.strokeEnd = end
layer.addSublayer(progressShape)
self.progressShapes.append(progressShape)
for _ in 1..<self._total {
start = CGFloat(end + CGFloat(pading))
end = CGFloat(Double(start + size))
print("------------------------")
print("start: \(start) | end: \(end) ")
let progressShape: CAShapeLayer!
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = start
progressShape.strokeEnd = end
layer.addSublayer(progressShape)
self.progressShapes.append(progressShape)
}
}
private func setProgressShapeFrame(){
guard self.progressShapes != nil else { return }
for shape in self.progressShapes {
shape.frame = bounds
let rect = rectForShape()
shape.path = pathForShape(rect: rect).cgPath
}
}
private func updateShapesWidth(){
guard self.progressShapes != nil else { return }
for i in 0..<self._total {
let shape = self.progressShapes[i]
let color = CGFloat(i) >= self.progress ? UIColor.white.withAlphaComponent(0.1).cgColor:progressShapeColor.cgColor
shape.strokeColor = color
shape.lineWidth = lineWidth - inset
shape.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
}
}
private func transformShapes(_ transform: CATransform3D){
guard self.progressShapes != nil else { return }
for shape in self.progressShapes {
shape.transform = transform
}
}
public func setProgress(progress: CGFloat, animated: Bool = true) {
self._progress = progress
// let actualProgress = progress/self._total
// if actualProgress > 1.0 {
// return
// }
//
// var start = progressShape.strokeEnd
// if let presentationLayer = progressShape.presentation(){
// if let count = progressShape.animationKeys()?.count, count > 0 {
// start = presentationLayer.strokeEnd
// }
// }
//
// let duration = abs(Double(progress - start)) * completeDuration
// percentLabel.text = String(format: percentLabelFormat, actualProgress * 100)
// progressShape.strokeEnd = actualProgress
//
// if animated {
// progressAnimation.fromValue = start
// progressAnimation.toValue = actualProgress
// progressAnimation.duration = duration
// progressShape.add(progressAnimation, forKey: progressAnimation.keyPath)
// }
}
// MARK: - Layout
open override func layoutSubviews() {
super.layoutSubviews()
backgroundShape.frame = bounds
let rect = rectForShape()
backgroundShape.path = pathForShape(rect: rect).cgPath
setProgressShapeFrame()
self.titleLabel.frame = CGRect(x: (self.bounds.size.width - titleLabelWidth)/2, y: self.bounds.size.height-50, width: titleLabelWidth, height: 42)
updateShapes()
percentLabel.frame = self.bounds
}
private func updateShapes() {
backgroundShape?.lineWidth = lineWidth
backgroundShape?.strokeColor = backgroundShapeColor.cgColor
backgroundShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
self.updateShapesWidth()
switch orientation {
case .left:
titleLabel.isHidden = true
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi / 2, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1.0)
case .right:
titleLabel.isHidden = true
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi * 1.5, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 1.5, 0, 0, 1.0)
case .bottom:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: temp.bounds.size.height-50, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi * 2, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 2, 0, 0, 1.0)
case .top:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: 0, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 0, 1.0)
}
}
// MARK: - Helper
private func rectForShape() -> CGRect {
return bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
}
private func pathForShape(rect: CGRect) -> UIBezierPath {
let startAngle:CGFloat!
let endAngle:CGFloat!
if clockwise{
startAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
endAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
}else{
startAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
endAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
}
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.width / 2.0, startAngle: startAngle, endAngle: endAngle
, clockwise: clockwise)
return path
}
}

Custom UIButton Causes Storyboard Agent Crash Swift

I created a custom UIButton class and it causing the storyboard agent to fail.
I'm Including my extensions cause I really don't know what the problem is.
I tried to debug this view from the storyboard but it sends me straight to assembly code.
I tried to make it a without #IBDesignable, but it still cause a crash.
Also if you tips for improving how I'm writing my class I'll be glad to hear them.
I'll be glad if you can help me
This is my class:
#IBDesignable class customButton: UIButton{
private let imagesPadding: CGFloat = 2
private var ArrowSymbleImageView: UIImageView!
#IBInspectable var iconImageInspectable: UIImage = UIImage(systemName: "globe")!{
willSet {
if (ArrowSymbleImageView != nil) {
ArrowSymbleImageView.image = newValue
}
}
}
#IBInspectable var BackgroundColorInspectable: UIColor = .white {
willSet {
self.backgroundColor = newValue
if (ArrowSymbleImageView != nil) {
if (self.BackgroundColorInspectable.isDarkColor) {
ArrowSymbleImageView.tintColor = .white
}else{
ArrowSymbleImageView.tintColor = .black
}
}
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.generalInit()
}
private func generalDeinit() {
ArrowSymbleImageView.removeFromSuperview()
}
private func generalInit() {
self.backgroundColor = self.BackgroundColorInspectable
self.roundCorners(corners: [.bottomLeft], radius: self.width() / 2 * 0.7)
self.dropShadow()
let sizePartFromView: CGFloat = 4
ArrowSymbleImageView = UIImageView(frame: CGRect(x: self.width() / 2 - (self.width() / sizePartFromView / 2),
y: self.height() / 2 - (self.height() / sizePartFromView / 2),
width: self.width() / sizePartFromView,
height: self.height() / sizePartFromView))
ArrowSymbleImageView.image = self.iconImageInspectable
if (self.BackgroundColorInspectable.isDarkColor) {
ArrowSymbleImageView.tintColor = .white
}else{
ArrowSymbleImageView.tintColor = .black
}
ArrowSymbleImageView.contentMode = .scaleAspectFill
self.addSubview(ArrowSymbleImageView)
}
}
internal extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
internal extension UIView {
func dropShadow(scale: Bool = true, size: CGSize = CGSize(width: -2, height: 2)) {
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.5
layer.shadowOffset = size
layer.shadowRadius = 1
layer.shadowPath = UIBezierPath(rect: bounds).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = scale ? UIScreen.main.scale : 1
}
func dropShadow(color: UIColor, opacity: Float = 0.5, offSet: CGSize, radius: CGFloat = 1, scale: Bool = true) {
layer.masksToBounds = false
layer.shadowColor = color.cgColor
layer.shadowOpacity = opacity
layer.shadowOffset = offSet
layer.shadowRadius = radius
layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = scale ? UIScreen.main.scale : 1
}
}
internal extension UIColor
{
var isDarkColor: Bool {
var r, g, b, a: CGFloat
(r, g, b, a) = (0, 0, 0, 0)
self.getRed(&r, green: &g, blue: &b, alpha: &a)
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
return lum < 0.50 ? true : false
}
}
I noticed this:
UIImage(systemName: "globe")!
Is there a reason why you instantiate a custom image this way? Is "globe" an apple provided default image?
You should be really using, if this isn't a default image.
UIImage(named:"globe")!
Dont use Force unwrapping when you do not have confirmation about data always use optional binding and do change following line in code from this UIImage(systemName: "globe")! to UIImage(named:"globe")! .

Side panel in Swift iPad

I have issues with my side panel for iPad app. I need buttons stacked as below:
Expected Output:
Right now, my output produces:
Current Output:
How can I remove circles and add button sets?
import UIKit
import QuartzCore
public protocol FrostedSidebarDelegate{
func sidebar(sidebar: FrostedSidebar, willShowOnScreenAnimated animated: Bool)
func sidebar(sidebar: FrostedSidebar, didShowOnScreenAnimated animated: Bool)
func sidebar(sidebar: FrostedSidebar, willDismissFromScreenAnimated animated: Bool)
func sidebar(sidebar: FrostedSidebar, didDismissFromScreenAnimated animated: Bool)
func sidebar(sidebar: FrostedSidebar, didTapItemAtIndex index: Int)
func sidebar(sidebar: FrostedSidebar, didEnable itemEnabled: Bool, itemAtIndex index: Int)
}
var sharedSidebar: FrostedSidebar?
public enum SidebarItemSelectionStyle{
case None
se Single
case All
}
public class FrostedSidebar: UIViewController {
public var width: CGFloat = 300.0
/**
If the sidebar should show from the right.
*/
public var showFromRight: Bool = false
/**
The speed at which the sidebar is presented/dismissed.
*/
public var animationDuration: CGFloat = 0.25
/**
The size of the sidebar items.
*/
public var itemSize: CGSize = CGSize(width: 200.0, height: 200.0)
/**
The background color of the sidebar items.
*/
public var itemBackgroundColor: UIColor = UIColor(white: 1, alpha: 0.25)
/**
The width of the ring around selected sidebar items.
*/
public var borderWidth: CGFloat = 2
/**
The sidebar's delegate.
*/
public var delegate: FrostedSidebarDelegate? = nil
/**
A dictionary that holds the actions for each item index.
*/
public var actionForIndex: [Int : ()->()] = [:]
/**
The indexes that are selected and have rings around them.
*/
public var selectedIndices: NSMutableIndexSet = NSMutableIndexSet()
/**
If the sidebar should be positioned beneath a navigation bar that is on screen.
*/
public var adjustForNavigationBar: Bool = false
/**
Returns whether or not the sidebar is currently being displayed
*/
public var isCurrentlyOpen: Bool = false
/**
The selection style for the sidebar.
*/
public var selectionStyle: SidebarItemSelectionStyle = .None{
didSet{
if case .All = selectionStyle{
selectedIndices = NSMutableIndexSet(indexesInRange: NSRange(location: 0, length: images.count))
}
}
}
//MARK: Private Properties
private var contentView: UIScrollView = UIScrollView()
private var blurView: UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .Dark))
private var dimView: UIView = UIView()
private var tapGesture: UITapGestureRecognizer? = nil
private var images: [UIImage] = []
private var borderColors: [UIColor]? = nil
private var itemViews: [CalloutItem] = []
//MARK: Public Methods
/**
Returns an object initialized from data in a given unarchiver.
*/
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
/**
Returns a sidebar initialized with the given data.
- Parameter itemImages: The images that will be used for each item.
- Parameter colors: The color of rings around each image.
- Parameter selectionStyle: The selection style for the sidebar.
- Precondition: `colors` is either `nil` or contains the same number of elements as `itemImages`.
*/
public init(itemImages: [UIImage], colors: [UIColor]?, selectionStyle: SidebarItemSelectionStyle){
contentView.alwaysBounceHorizontal = false
contentView.alwaysBounceVertical = true
contentView.bounces = true
contentView.clipsToBounds = false
contentView.showsHorizontalScrollIndicator = false
contentView.showsVerticalScrollIndicator = false
if let colors = colors{
assert(itemImages.count == colors.count, "If item color are supplied, the itemImages and colors arrays must be of the same size.")
}
self.selectionStyle = selectionStyle
borderColors = colors
images = itemImages
for (index, image) in images.enumerate(){
let view = CalloutItem(index: index)
view.clipsToBounds = true
view.imageView.image = image
contentView.addSubview(view)
itemViews += [view]
if let borderColors = borderColors{
if selectedIndices.containsIndex(index){
let color = borderColors[index]
view.layer.borderColor = color.CGColor
}
} else{
view.layer.borderColor = UIColor.clearColor().CGColor
}
}
super.init(nibName: nil, bundle: nil)
}
public override func shouldAutorotate() -> Bool {
return true
}
public override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.All
}
public override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
if isViewLoaded(){
dismissAnimated(false, completion: nil)
}
}
public override func loadView() {
super.loadView()
view.backgroundColor = UIColor.clearColor()
view.addSubview(dimView)
view.addSubview(blurView)
view.addSubview(contentView)
tapGesture = UITapGestureRecognizer(target: self, action: #selector(FrostedSidebar.handleTap(_:)))
view.addGestureRecognizer(tapGesture!)
}
/**
Shows the sidebar in a view controller.
- Parameter viewController: The view controller in which to show the sidebar.
- Parameter animated: If the sidebar should be animated.
*/
public func showInViewController(viewController: UIViewController, animated: Bool){
layoutItems()
if let bar = sharedSidebar{
bar.dismissAnimated(false, completion: nil)
}
delegate?.sidebar(self, willShowOnScreenAnimated: animated)
sharedSidebar = self
addToParentViewController(viewController, callingAppearanceMethods: true)
view.frame = viewController.view.bounds
dimView.backgroundColor = UIColor.blackColor()
dimView.alpha = 0
dimView.frame = view.bounds
let parentWidth = view.bounds.size.width
var contentFrame = view.bounds
contentFrame.origin.x = showFromRight ? parentWidth : -width
contentFrame.size.width = width
contentView.frame = contentFrame
contentView.contentOffset = CGPoint(x: 0, y: 0)
layoutItems()
var blurFrame = CGRect(x: showFromRight ? view.bounds.size.width : 0, y: 0, width: 0, height: view.bounds.size.height)
blurView.frame = blurFrame
blurView.contentMode = showFromRight ? UIViewContentMode.TopRight : UIViewContentMode.TopLeft
blurView.clipsToBounds = true
view.insertSubview(blurView, belowSubview: contentView)
contentFrame.origin.x = showFromRight ? parentWidth - width : 0
blurFrame.origin.x = contentFrame.origin.x
blurFrame.size.width = width
let animations: () -> () = {
self.contentView.frame = contentFrame
self.blurView.frame = blurFrame
self.dimView.alpha = 0.25
}
let completion: (Bool) -> Void = { finished in
if finished{
self.delegate?.sidebar(self, didShowOnScreenAnimated: animated)
}
}
if animated{
UIView.animateWithDuration(NSTimeInterval(animationDuration), delay: 0, options: UIViewAnimationOptions(), animations: animations, completion: completion)
} else{
animations()
completion(true)
}
for (index, item) in itemViews.enumerate(){
item.layer.transform = CATransform3DMakeScale(0.3, 0.3, 1)
item.alpha = 0
item.originalBackgroundColor = itemBackgroundColor
item.layer.borderWidth = borderWidth
animateSpringWithView(item, idx: index, initDelay: animationDuration)
}
self.isCurrentlyOpen = true
}
/**
Dismisses the sidebar.
- Parameter animated: If the sidebar should be animated.
- Parameter completion: Completion handler called when the sidebar is dismissed.
*/
public func dismissAnimated(animated: Bool, completion: ((Bool) -> Void)?){
let completionBlock: (Bool) -> Void = {finished in
self.removeFromParentViewControllerCallingAppearanceMethods(true)
self.delegate?.sidebar(self, didDismissFromScreenAnimated: true)
self.layoutItems()
if let completion = completion{
completion(finished)
}
}
delegate?.sidebar(self, willDismissFromScreenAnimated: animated)
if animated{
let parentWidth = view.bounds.size.width
var contentFrame = contentView.frame
contentFrame.origin.x = showFromRight ? parentWidth : -width
var blurFrame = blurView.frame
blurFrame.origin.x = showFromRight ? parentWidth : 0
blurFrame.size.width = 0
UIView.animateWithDuration(NSTimeInterval(animationDuration), delay: 0, options: UIViewAnimationOptions.BeginFromCurrentState, animations: {
self.contentView.frame = contentFrame
self.blurView.frame = blurFrame
self.dimView.alpha = 0
}, completion: completionBlock)
} else{
completionBlock(true)
}
self.isCurrentlyOpen = false
}
/**
Selects the item at the given index.
- Parameter index: The index of the item to select.
*/
public func selectItemAtIndex(index: Int){
let didEnable = !selectedIndices.containsIndex(index)
if let borderColors = borderColors{
let stroke = borderColors[index]
let item = itemViews[index]
if didEnable{
if case .Single = selectionStyle{
selectedIndices.removeAllIndexes()
for item in itemViews{
item.layer.borderColor = UIColor.clearColor().CGColor
}
}
item.layer.borderColor = stroke.CGColor
let borderAnimation = CABasicAnimation(keyPath: "borderColor")
borderAnimation.fromValue = UIColor.clearColor().CGColor
borderAnimation.toValue = stroke.CGColor
borderAnimation.duration = 0.5
item.layer.addAnimation(borderAnimation, forKey: nil)
selectedIndices.addIndex(index)
} else{
if case .None = selectionStyle{
item.layer.borderColor = UIColor.clearColor().CGColor
selectedIndices.removeIndex(index)
}
}
let pathFrame = CGRect(x: -CGRectGetMidX(item.bounds), y: -CGRectGetMidY(item.bounds), width: item.bounds.size.width, height: item.bounds.size.height)
let path = UIBezierPath(roundedRect: pathFrame, cornerRadius: item.layer.cornerRadius)
let shapePosition = view.convertPoint(item.center, fromView: contentView)
let circleShape = CAShapeLayer()
circleShape.path = path.CGPath
circleShape.position = shapePosition
circleShape.fillColor = UIColor.clearColor().CGColor
circleShape.opacity = 0
circleShape.strokeColor = stroke.CGColor
circleShape.lineWidth = borderWidth
view.layer.addSublayer(circleShape)
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
scaleAnimation.toValue = NSValue(CATransform3D: CATransform3DMakeScale(2.5, 2.5, 1))
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0
let animation = CAAnimationGroup()
animation.animations = [scaleAnimation, alphaAnimation]
animation.duration = 0.5
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
circleShape.addAnimation(animation, forKey: nil)
}
if let action = actionForIndex[index]{
action()
}
delegate?.sidebar(self, didTapItemAtIndex: index)
delegate?.sidebar(self, didEnable: didEnable, itemAtIndex: index)
}
//MARK: Private Classes
private class CalloutItem: UIView{
var imageView: UIImageView = UIImageView()
var itemIndex: Int
var originalBackgroundColor:UIColor? {
didSet{
backgroundColor = originalBackgroundColor
}
}
required init?(coder aDecoder: NSCoder) {
itemIndex = 0
super.init(coder: aDecoder)
}
init(index: Int){
imageView.backgroundColor = UIColor.clearColor()
imageView.contentMode = UIViewContentMode.ScaleAspectFit
itemIndex = index
super.init(frame: CGRect.zero)
addSubview(imageView)
}
override func layoutSubviews() {
super.layoutSubviews()
let inset: CGFloat = bounds.size.height/2
imageView.frame = CGRect(x: 0, y: 0, width: inset, height: inset)
imageView.center = CGPoint(x: inset, y: inset)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
let darkenFactor: CGFloat = 0.3
var darkerColor: UIColor
if originalBackgroundColor != nil && originalBackgroundColor!.getRed(&r, green: &g, blue: &b, alpha: &a){
darkerColor = UIColor(red: max(r - darkenFactor, 0), green: max(g - darkenFactor, 0), blue: max(b - darkenFactor, 0), alpha: a)
} else if originalBackgroundColor != nil && originalBackgroundColor!.getWhite(&r, alpha: &a){
darkerColor = UIColor(white: max(r - darkenFactor, 0), alpha: a)
} else{
darkerColor = UIColor.clearColor()
assert(false, "Item color should be RBG of White/Alpha in order to darken the button")
}
backgroundColor = darkerColor
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
backgroundColor = originalBackgroundColor
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
super.touchesCancelled(touches, withEvent: event)
backgroundColor = originalBackgroundColor
}
}
//MARK: Private Methods
private func animateSpringWithView(view: CalloutItem, idx: Int, initDelay: CGFloat){
let delay: NSTimeInterval = NSTimeInterval(initDelay) + NSTimeInterval(idx) * 0.1
UIView.animateWithDuration(0.5,
delay: delay,
usingSpringWithDamping: 10.0,
initialSpringVelocity: 50.0,
options: UIViewAnimationOptions.BeginFromCurrentState,
animations: {
view.layer.transform = CATransform3DIdentity
view.alpha = 1
},
completion: nil)
}
#objc private func handleTap(recognizer: UITapGestureRecognizer){
let location = recognizer.locationInView(view)
if !CGRectContainsPoint(contentView.frame, location){
dismissAnimated(true, completion: nil)
} else{
let tapIndex = indexOfTap(recognizer.locationInView(contentView))
if let tapIndex = tapIndex{
selectItemAtIndex(tapIndex)
}
}
}
private func layoutSubviews(){
let x = showFromRight ? parentViewController!.view.bounds.size.width - width : 0
contentView.frame = CGRect(x: x, y: 0, width: width, height: parentViewController!.view.bounds.size.height)
blurView.frame = contentView.frame
layoutItems()
}
private func layoutItems(){
let leftPadding: CGFloat = (width - itemSize.width) / 2
let topPadding: CGFloat = leftPadding
for (index, item) in itemViews.enumerate(){
let idx: CGFloat = adjustForNavigationBar ? CGFloat(index) + 0.5 : CGFloat(index)
let frame = CGRect(x: leftPadding, y: topPadding*idx + itemSize.height*idx + topPadding, width:itemSize.width, height: itemSize.height)
item.frame = frame
item.layer.cornerRadius = frame.size.width / 2
item.layer.borderColor = UIColor.clearColor().CGColor
item.alpha = 0
if selectedIndices.containsIndex(index){
if let borderColors = borderColors{
item.layer.borderColor = borderColors[index].CGColor
}
}
}
let itemCount = CGFloat(itemViews.count)
if adjustForNavigationBar{
contentView.contentSize = CGSizeMake(0, (itemCount + 0.5) * (itemSize.height + topPadding) + topPadding)
} else {
contentView.contentSize = CGSizeMake(0, itemCount * (itemSize.height + topPadding) + topPadding)
}
}
private func indexOfTap(location: CGPoint) -> Int? {
var index: Int?
for (idx, item) in itemViews.enumerate(){
if CGRectContainsPoint(item.frame, location){
index = idx
break
}
}
return index
}
private func addToParentViewController(viewController: UIViewController, callingAppearanceMethods: Bool){
if let _ = parentViewController{
removeFromParentViewControllerCallingAppearanceMethods(callingAppearanceMethods)
}
if callingAppearanceMethods{
beginAppearanceTransition(true, animated: false)
}
viewController.addChildViewController(self)
viewController.view.addSubview(view)
didMoveToParentViewController(self)
if callingAppearanceMethods{
endAppearanceTransition()
}
}
private func removeFromParentViewControllerCallingAppearanceMethods(callAppearanceMethods: Bool){
if callAppearanceMethods{
beginAppearanceTransition(false, animated: false)
}
willMoveToParentViewController(nil)
view.removeFromSuperview()
removeFromParentViewController()
if callAppearanceMethods{
endAppearanceTransition()
}
}
}
Instead of putting each button in it's own view, you need to create a view that contains 3 buttons and then add the circle to the view.