I am trying to create a custom header file to all the ViewControllers in project. The following image is an actual design.
So that I am trying to create Custom view with Xib file as below :
import UIKit
class HamburgrView: UIView {
override init(frame: CGRect) {
// 1. setup any properties here
// 2. call super.init(frame:)
super.init(frame: frame)
// 3. Setup view from .xib file
xibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
override open func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
xibSetup()
}
override func layoutSubviews() {
}
func xibSetup() {
let tempView = loadViewFromNib()
// use bounds not frame or it'll be offset
tempView.frame = bounds
// Make the view stretch with containing view
tempView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(tempView)
}
func loadViewFromNib() -> UIView {
let bundle = Bundle(for: type(of:self))
let nib = UINib(nibName: "HamburgrView", bundle: bundle)
// Assumes UIView is top level and only object in CustomView.xib file
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
return view
}
}
We don't know how to draw that Hamburger menu icon with curve and its start position from SafeArealayoutGuide.
I trying one view with x position negative -40 and giving cornerRadius not coming perfect curve like above image.
Output look not good.
My ViewController Code :
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let hamView = HamburgrView(frame: .zero)
self.view.addSubview(hamView)
hamView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hamView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 0),
hamView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0),
hamView.widthAnchor.constraint(equalToConstant: 120),
hamView.heightAnchor.constraint(equalToConstant: 80)
])
}
As #sandip answer I have changed like below :
func xibSetup() {
let tempView = loadViewFromNib()
// use bounds not frame or it'll be offset
tempView.frame = bounds
// Make the view stretch with containing view
tempView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(tempView)
let layer = CAShapeLayer()
layer.path = UIBezierPath.init(arcCenter: CGPoint(x: 0, y: self.center.y), radius: 50, startAngle: -1.5708, endAngle: 1.5708, clockwise: true).cgPath
layer.fillColor = UIColor.red.cgColor
self.layer.addSublayer(layer)
}
Output Looks Below :
Any idea would be appreciate.
Is this what you wanted?
You can easily achieve it with CAShapeLayer and UIBezierPath
let layer = CAShapeLayer()
layer.path = UIBezierPath.init(arcCenter: CGPoint(x: 0, y: self.view.center.y), radius: 30, startAngle: -1.5708, endAngle: 1.5708, clockwise: true).cgPath
layer.fillColor = UIColor.red.cgColor
view.layer.addSublayer(layer)
In your code change arc centre as x to be zero and y to hamburger view's vertical centre and add it to self.view
Hope it helps
EDIT 1:
As OP has asked for complete code in comment updating the answer
class HamburgrView: UIView {
override init(frame: CGRect) {
// 1. setup any properties here
// 2. call super.init(frame:)
super.init(frame: frame)
// 3. Setup view from .xib file
xibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
override open func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
xibSetup()
}
override func layoutSubviews() {
}
func xibSetup() {
let tempView = loadViewFromNib()
// use bounds not frame or it'll be offset
tempView.frame = bounds
// Make the view stretch with containing view
tempView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(tempView)
}
func loadViewFromNib() -> UIView {
let bundle = Bundle(for: type(of:self))
let nib = UINib(nibName: "HamburgrView", bundle: bundle)
// Assumes UIView is top level and only object in CustomView.xib file
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
return view
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
let layer = CAShapeLayer()
layer.path = UIBezierPath.init(arcCenter: CGPoint(x: 0, y: 40), radius: 50, startAngle: -1.5708, endAngle: 1.5708, clockwise: true).cgPath
layer.fillColor = UIColor.red.cgColor
self.layer.addSublayer(layer)
}
}
EDIT 2:
Reason OPs arc coming outside hamburger view is because of wrong arc centre
changing to CGPoint(x: 0, y: 40) from self.view.center.y should solve the issue. Updated the code to reflect the same
Why 40?
Because OP is setting height of hamburger view to 80. So 80/2 = 40
Why cant we use bounds.size.height / 2 or self.view.center.y?
because in didMoveToSuperview OP's constraint has not kicked in yet so it is taking the height view in xib which I don't think is 80 so it sets the arc centre with that value but then your constraint changes height to 80.
Here is final O/P:
Related
I have been trying to create a a custom "slider" or knob with UIControl for my app. I found this tutorial and have been using it for some inspiration, but since it does not really accomplish what I want to do I am using it as more of a reference than a tutorial. Anyways, I wrote the code below and found that my CAShapeLayer was not in the center of the UIView that I set to be an instance of my custom class CircularSelector.
Here is my code:
class CircularSelector: UIControl {
private let renderer = CircularSelectorRenderer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
renderer.updateBounds(bounds)
//layer.borderWidth = 4
// layer.borderColor = UIColor.red.cgColor
layer.addSublayer(renderer.selectorLayer)
}
}
class CircularSelectorRenderer {
let selectorLayer = CAShapeLayer()
init() {
selectorLayer.fillColor = UIColor.clear.cgColor
selectorLayer.strokeColor = UIColor.white.cgColor
//Testing
//selectorLayer.borderColor = UIColor.white.cgColor
// selectorLayer.borderWidth = 4
}
private func updateSelectorLayerPath() {
let bounds = selectorLayer.bounds
let arcCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let radius = 125
var ring = UIBezierPath(arcCenter: arcCenter, radius: CGFloat(radius), startAngle: 0.degreesToRadians, endAngle: 180.degreesToRadians, clockwise: false)
selectorLayer.lineWidth = 10
selectorLayer.path = ring.cgPath
}
func updateBounds(_ bounds: CGRect) {
selectorLayer.bounds = bounds
selectorLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
updateSelectorLayerPath()
}
}
This is what I get:
and when I uncomment the code under the //Testing line in the init() of the CircularSelectorRenderer I get this:
The grey UIView is of the class CircularSelector. I am confused as to why my CAShapeLayer is wider than the grey view itself and why it is not in the center of the grey view. Why is this happening and how can I fix it.
The problem is that you are updating the shape in the wrong place. Views can (and will) change sizes for different device sizes, superview sizes, device rotation, etc.
You want to update your shape layer when your view is told to layout its subviews:
class CircularSelector: UIControl {
private let renderer = CircularSelectorRenderer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// no need to call this here
//renderer.updateBounds(bounds)
//layer.borderWidth = 4
// layer.borderColor = UIColor.red.cgColor
layer.addSublayer(renderer.selectorLayer)
}
// add this func
override func layoutSubviews() {
super.layoutSubviews()
// here is where you want to update the layer
renderer.updateBounds(bounds)
}
}
my custom uislider background uiview is offset from original view frame.
and the further the the slider original (x,y) from 0,0 the more the offset.
Please check the image below.
uislider subview frame offset
import UIKit
class customSlier: UISlider {
override func awakeFromNib() {
super.awakeFromNib()
self.initializeSomeSettings()
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func initializeSomeSettings() {
let view: UIView = UIView()
view.frame = self.frame
view.backgroundColor = UIColor.yellow
self.insertSubview(view, belowSubview: self)
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
I think the problem is at this line:
view.frame = self.frame
Let's say your scroller is at position (10, 10). Then the background frame is also at position (10,10) in the coordinator of the scroll frame, which is (20,20) in the original view. My suggestion is to replace this line by this:
view.frame = CGRect(origin: .zero, size: self.frame.size)
Hope this helps.
try this
view.frame = self.bounds
I'm building a custom view using an XIB file. However, I am facing a problem where the layers I add to the view (trackLayer) are not shown on the xib (circleLayer is an animation and I don't expect it to render in xib which is not possible to my knowledge). The code of the owner class for the XIB is shown as follows:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: SpinningView!
//MARK: Properties
let circleLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let circularAnimation: CABasicAnimation = {
let animation = CABasicAnimation()
animation.fromValue = 0
animation.toValue = 1
animation.duration = 2
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
return animation
}()
//MARK: IB Inspectables
#IBInspectable var strokeColor: UIColor = UIColor.red {
...
}
#IBInspectable var trackColor: UIColor = UIColor.lightGray {
...
}
#IBInspectable var lineWidth: CGFloat = 5 {
...
}
#IBInspectable var fillColor: UIColor = UIColor.clear {
...
}
#IBInspectable var isAnimated: Bool = true {
...
}
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
initSubviews()
}
//MARK: Private functions
private func setup() {
setupTrack()
setupAnimationOnTrack()
}
private func setupTrack(){
trackLayer.lineWidth = lineWidth
trackLayer.fillColor = fillColor.cgColor
trackLayer.strokeColor = trackColor.cgColor
layer.addSublayer(trackLayer)
}
private func setupAnimationOnTrack(){
circleLayer.lineWidth = lineWidth
circleLayer.fillColor = fillColor.cgColor
circleLayer.strokeColor = strokeColor.cgColor
circleLayer.lineCap = kCALineCapRound
layer.addSublayer(circleLayer)
updateAnimation()
}
private func updateAnimation() {
if isAnimated {
circleLayer.add(circularAnimation, forKey: "strokeEnd")
}
else {
circleLayer.removeAnimation(forKey: "strokeEnd")
}
}
//MARK: Layout contraints
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width / 2, bounds.height / 2) - circleLayer.lineWidth / 2
let startAngle = -CGFloat.pi / 2
let endAngle = 3 * CGFloat.pi / 2
let path = UIBezierPath(arcCenter: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
trackLayer.position = center
trackLayer.path = path.cgPath
circleLayer.position = center
circleLayer.path = path.cgPath
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
}
}
When a view is subclassed in the Main.storyboard, I can see the image and the tracklayer as follows IB UIView but when I go over to the XIB, it does not have the trackLayer(circle around the image) XIB Image. While one can argue that it is working and why I am bothering with this, I think it is important that I design the XIB properly since another person might just see it as an view with only an image (no idea of the animating feature)
When you are designing your XIB, it is not an instance of your #IBDesignable SpinningView. You are, effectively, looking at the source of your SpinningView.
So, the code behind it is not running at that point - it only runs when you add an instance of SpinningView to another view.
Take a look at this simple example:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet var theLabel: UILabel!
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
// this will change the label's textColor in Storyboard
// when a UIView's class is set to SpinningView
theLabel.textColor = .red
}
}
When I am designing the XIB, this is what I see:
But when I add a UIView to another view, and assign its class as SpinningView, this is what I see:
The label's text color get's changed to red, because the code is now running.
Another way to think about it... you have trackColor, fillColor, etc vars defined as #IBInspectable. When you're designing your XIB, you don't see those properties in Xcode's Attributes Inspector panel - you only see them when you've selected an instance of SpinningView that has been added elsewhere.
Hope that makes sense.
I'm having issues drawing a circle using CAShapeLayer. I have a custom view MainLogoAnimationView code as follows:
import UIKit
class MainLogoAnimationView: UIView {
#IBOutlet var customView: UIView!
var redCircle: AnimationCircleView!
// MARK: Object Lifecycle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
_ = Bundle.main.loadNibNamed("MainLogoAnimationView", owner: self, options: nil)?[0] as! UIView
self.addSubview(customView)
customView.frame = self.bounds
let circleWidth = frame.size.width*0.25
let yPos = frame.size.height/2 - circleWidth/2
redCircle = AnimationCircleView(frame: CGRect(x: 10, y: yPos, width: circleWidth, height: circleWidth))
redCircle.backgroundColor = UIColor.yellow
addSubview(redCircle)
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
In interface builder I created a light blue UIView and selected MainLogoAnimationView as the subview.
The AnimationCircleView is like so:
import UIKit
class AnimationCircleView: UIView {
// MARK: Object lifecycle
var circleColor = UIColor.red.withAlphaComponent(0.5).cgColor {
didSet {
shapeLayer.fillColor = circleColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
let shapeLayer = CAShapeLayer()
func setup() {
let circlePath = UIBezierPath(ovalIn: frame)
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor
shapeLayer.frame = self.frame
shapeLayer.backgroundColor = UIColor.gray.cgColor
layer.addSublayer(shapeLayer)
}
}
Screenshot:
Is seems the CAShapeLayer frame is incorrect and also the red circle is in the wrong position. It should be inside the yellow square. What am I doing wrong here? Any pointers would be really appreciated! Thanks!
UPDATE:
In your AnimationCircleView inside setup change
shapeLayer.frame = self.frame
to
shapeLayer.frame = self.bounds
This should ensure the gray colour is drawn inside the yellow rectangle.
Edit:
Similar to the answer above, changing
let circlePath = UIBezierPath(ovalIn: frame)
to
let circlePath = UIBezierPath(ovalIn: bounds)
will solve your problem and draw circle inside the gray area.
CAShapeLayer frame is calculated relative to the position of the view it is added on, soo setting it equal to the frame, adds the CAShapeLayer at a x and y position offset from the origin by the value of the x and y passed in self.frame.
I made a simple custom view in Swift which has a background image view with a photo and some labels in front. I then added the custom view to my storyboard and it displays well, I can see the background image and labels.
But when I run my app on my device the image view doesn't show immediately, if I navigate to another view then back, it is displayed. I only have a timer which scheduled some tasks in background in my scene. Did I miss anything here?
Here is the code of my custom view
import UIKit
#IBDesignable class GaugeView: UIImageView {
#IBOutlet weak var speedLabel: UILabel!
let circlePathLayer = CAShapeLayer()
var circleRadius: CGFloat = 50.0
var speed: CGFloat {
get {
return circlePathLayer.strokeEnd
}
set {
speedLabel.text = String(format: "%.2f", newValue)
if newValue < 20.0 {
circlePathLayer.strokeColor = UIColor.blueColor().CGColor
} else if newValue >= 25.0 {
circlePathLayer.strokeColor = UIColor.redColor().CGColor
} else {
circlePathLayer.strokeColor = UIColor.yellowColor().CGColor
}
if (newValue > 50) {
circlePathLayer.strokeEnd = 1
} else if (newValue < 0) {
circlePathLayer.strokeEnd = 0
} else {
circlePathLayer.strokeEnd = newValue / 49.9
}
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
override func layoutSubviews() {
super.layoutSubviews()
circleRadius = bounds.width / 2 - 2.6
circlePathLayer.frame = bounds
circlePathLayer.path = circlePath().CGPath
}
// Our custom view from the XIB file
var view: UIView!
let nibName = "GaugeView"
func xibSetup() {
view = loadViewFromNib()
// use bounds not frame or it'll be offset
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(view)
// Configure speed circle layer
configure()
}
func configure() {
speed = 0.0
circlePathLayer.frame = bounds
circlePathLayer.lineWidth = 6
circlePathLayer.fillColor = UIColor.clearColor().CGColor
circlePathLayer.strokeColor = UIColor.blueColor().CGColor
layer.addSublayer(circlePathLayer)
backgroundColor = UIColor.whiteColor()
}
func circleFrame() -> CGRect {
var circleFrame = CGRect(x: 0, y: 0, width: 2*circleRadius, height: 2*circleRadius)
circleFrame.origin.x = CGRectGetMidX(circlePathLayer.bounds) - CGRectGetMidX(circleFrame)
circleFrame.origin.y = CGRectGetMidY(circlePathLayer.bounds) - CGRectGetMidY(circleFrame)
return circleFrame
}
func circlePath() -> UIBezierPath {
let center: CGPoint = CGPointMake(CGRectGetMidX(circlePathLayer.bounds), CGRectGetMidY(circlePathLayer.bounds))
let start = CGFloat(1.33 * M_PI_2)
let end = CGFloat( 1.64 * M_2_PI)
return UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: start, endAngle: end, clockwise: true)
}
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
}
Then I added the custom view to my storyboard and navigate to the scene programatically with performSegueWithIdentifier("dashboardSegue", sender: nil). I just noticed not only the custome view, but two progress bar on the scene are not displayed as well.
It's better show your code, but i am guessing your timer stuck the main UI thread. When you navigate to another view, your timer.invalidate() was called. Try to remove the NSTimer to see if it works well.