Tried making a custom ScrollView, but instead of scrolling it's spamming up and down - swift

I tried to create some kind of timeline (with the Vector Illustrator mentality), using UIBezier and UI Label (kind of like in the calendar app) and then use UIPanGestureRecognizer to scroll it up and down. But whenever I scroll it in the simulator, it multiplies itself instead of moving like the images below (I use setNeedsDisplay as the scrollValue changes to redraw the whole mechanism). This is probably a small mistake a I did or maybe my code doesn't work.
I know I could use a UIScrollView or UITableView instead, but I tried making this as a small challenge as a custom made table because using pre-made objects feels limiting for someone like me who is used to CAD drawing or Vector Illustrator.
This image explains what happens in the Simulator:
The code I used is below:
import UIKit
class ViewController: UIViewController {
var tlobject = TimelineView()
let gesto = UIPanGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
// ===== Add TimelineView Object to view
let TLObjectFrame = CGRect(x: 0, y: 40, width: 100, height: 100)
tlobject = TimelineView(frame: TLObjectFrame)
view.addSubview(tlobject)
// ===== ADD TOUCH GESTURE =====
gesto.addTarget(self, action: #selector(touchinput))
view.addGestureRecognizer(gesto)
}
var touchStartLocation: Int = 0
var scrollDistance: Int = 0
var lastScrollDistance: Int = 0
//The following func calculates the distance scrolled/travelled by Touch gesture on the YAxis and sends the result value (scrollDistance) to the Timeline mechanism where it defines the Yposition of every UIBezier. Thanks to Mitchell Hudson on Youtube for helping me figure out how to do it on his Tutorial "06 11 touches value"
#objc func touchinput (sender: UIPanGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.began {
touchStartLocation = Int(sender.location(in: view).y)
lastScrollDistance = scrollDistance
}
if sender.state == UIGestureRecognizer.State.changed {
let touchEndLocation = Int(sender.location(in: view).y)
let currentScrollDistance = touchEndLocation - touchStartLocation
print("deltaY", currentScrollDistance)
var newScrollDistance = lastScrollDistance + currentScrollDistance
scrollDistance = newScrollDistance
tlobject.totalScrollDistance = scrollDistance //send scrollValue to TimelineView
}
if sender.state == UIGestureRecognizer.State.ended {
print("lastScrollDistance", lastScrollDistance)
print("scroll Distance", scrollDistance)
}
}
}
//Created a new View with the TimeLine mechanism
class TimelineView: UIView {
var totalScrollDistance: Int = 0 {
didSet{
setNeedsDisplay() //this gets called everytime UIgesture position changes
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
timelinemechanism()
}
func timelinemechanism() {
let lineElements: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let spacing: Int = 30
let scrollDistance: Int = totalScrollDistance
let totalElements: Int = lineElements.count
for n in 1...totalElements {
//Get UILabel/UILine Yposition on screen = Array index number * the spacing + scroll distance by touch pan gesture
let yPosition = lineElements[n - 1] * spacing + scrollDistance
let linepath = UIBezierPath()
linepath.move(to: CGPoint(x: 60, y: yPosition))
linepath.addLine(to: CGPoint(x: 300, y: yPosition))
let lineshape = CAShapeLayer()
lineshape.path = linepath.cgPath
lineshape.strokeColor = UIColor.blue.cgColor
//lineshape.fillColor = UIColor.white.cgColor
//lineshape.lineWidth = 1
self.layer.addSublayer(lineshape)
let hourlabel = UILabel()
hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40)
hourlabel.text = "\(n):00"
//hourlabel.font = UIFont(name: "Avenir-Claro", size: 12)
hourlabel.textColor = UIColor.blue
hourlabel.textAlignment = NSTextAlignment.right
self.addSubview(hourlabel)
}
}
}

Inside draw you only have to draw something. You add new subviews/sublayers and do not remove old ones.
Creating a new view every time you change a frame is very resource-intensive. And you don't need that, because you have the same views, you only need to change the position.
Instead, you can create your views at start and use layoutSubviews to update your views positions:
class TimelineView: UIView {
var totalScrollDistance: Int = 0 {
didSet{
setNeedsLayout() //this gets called everytime UIgesture position changes
}
}
private var lastLayoutTotalScrollDistance: Int = 0
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
createTimelinemechanism()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var lineShapes = [CAShapeLayer]()
var hourLabels = [UILabel]()
override func layoutSubviews() {
super.layoutSubviews()
let offset = totalScrollDistance - lastLayoutTotalScrollDistance
lastLayoutTotalScrollDistance = totalScrollDistance
lineShapes.forEach { lineShape in
lineShape.frame.origin.y += CGFloat(offset)
}
hourLabels.forEach { hourLabel in
hourLabel.frame.origin.y += CGFloat(offset)
}
}
func createTimelinemechanism() {
let lineElements: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let spacing: Int = 30
let totalElements: Int = lineElements.count
for n in 1...totalElements {
//Get UILabel/UILine Yposition on screen = Array index number * the spacing + scroll distance by touch pan gesture
let yPosition = lineElements[n - 1] * spacing
let linepath = UIBezierPath()
linepath.move(to: CGPoint(x: 60, y: yPosition))
linepath.addLine(to: CGPoint(x: 300, y: yPosition))
let lineshape = CAShapeLayer()
lineshape.path = linepath.cgPath
lineshape.strokeColor = UIColor.blue.cgColor
//lineshape.fillColor = UIColor.white.cgColor
//lineshape.lineWidth = 1
// disable default layer position animation
lineshape.actions = [
"position": NSNull(),
]
self.layer.addSublayer(lineshape)
lineShapes.append(lineshape)
let hourlabel = UILabel()
hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40)
hourlabel.text = "\(n):00"
//hourlabel.font = UIFont(name: "Avenir-Claro", size: 12)
hourlabel.textColor = UIColor.blue
hourlabel.textAlignment = NSTextAlignment.right
self.addSubview(hourlabel)
hourLabels.append(hourlabel)
}
}
}
More generally, you can just list all the subviews/sublayers and not keep them in separate containers.

I spent a bit more time with your question since my first thought was wrong. Let me start by saying that your approach here is not the right way to go about this. But it looks to me like you're playing with different aspects of the framework just to learn your way around and I can respect that. I spent many years working on as vector drawing program (Macromedia FreeHand) and even wrote a book about drawing with Quartz 2D back in 2006 so I understand the desire to draw it yourself.
I've reworked your example using "raw" drawing at the CGContext level. I was playing with your code in a Playground so I restructured the view creation a bit too (just so it shows up in the Playground nicely). You should be able to copy and paste this into an iOS playground and see the results.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let gesto = UIPanGestureRecognizer()
let timelineView = TimelineView()
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = true
self.view.backgroundColor = UIColor.red
self.view.bounds = CGRect(x:0, y:0, width: 320, height: 700)
// ===== Add TimelineView Object to view
view.addSubview(timelineView)
timelineView.frame = CGRect(x: 20, y: 20, width: 280, height: 660)
timelineView.backgroundColor = UIColor.white
debugPrint(timelineView.bounds)
// ===== ADD TOUCH GESTURE =====
gesto.addTarget(self, action: #selector(touchinput))
timelineView.addGestureRecognizer(gesto)
}
var touchStartLocation: Int = 0
var scrollDistance: Int = 0
var lastScrollDistance: Int = 0
//The following func calculates the distance scrolled/travelled by Touch gesture on the YAxis and sends the result value (scrollDistance) to the Timeline mechanism where it defines the Yposition of every UIBezier. Thanks to Mitchell Hudson on Youtube for helping me figure out how to do it on his Tutorial "06 11 touches value"
#objc func touchinput (sender: UIPanGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.began {
touchStartLocation = Int(sender.location(in: view).y)
lastScrollDistance = scrollDistance
}
if sender.state == UIGestureRecognizer.State.changed {
let touchEndLocation = Int(sender.location(in: view).y)
let currentScrollDistance = touchEndLocation - touchStartLocation
print("deltaY", currentScrollDistance)
scrollDistance = lastScrollDistance + currentScrollDistance
timelineView.totalScrollDistance = scrollDistance //send scrollValue to TimelineView
}
if sender.state == UIGestureRecognizer.State.ended {
print("lastScrollDistance", lastScrollDistance)
print("scroll Distance", scrollDistance)
}
}
}
//Created a new View with the TimeLine mechanism
class TimelineView: UIView {
var totalScrollDistance: Int = 0 {
didSet {
setNeedsDisplay() //this gets called everytime UIgesture position changes
}
}
override func draw(_ rect: CGRect) {
if let cgContext = UIGraphicsGetCurrentContext() {
drawTimeline(cgContext: cgContext)
}
}
func drawTimeline(cgContext: CGContext) {
let numElements = 10
let spacing = 30
let scrollDistance = totalScrollDistance
for n in 0..<numElements {
let yPosition = n * spacing + scrollDistance
cgContext.saveGState()
cgContext.setLineWidth(1.0)
cgContext.setStrokeColor(UIColor.blue.cgColor)
cgContext.move(to: CGPoint(x: 60, y: yPosition))
cgContext.addLine(to: CGPoint(x: 300, y: yPosition))
cgContext.strokePath()
let label : NSString = "\(n):00" as NSString
label.draw(at: CGPoint(x: 5, y: yPosition - 20),
withAttributes: [.foregroundColor : UIColor.blue])
cgContext.restoreGState()
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = ViewController()
The drawRect of the custom view grabs the current CGContext and passes it to the routine that does the drawing. Using something like UIBezierPath will work, of course (you saw that it did) but it has overhead (creating an actual object, copying the object into the context graphics state on each drawing, etc) that you don't necessarily need.
I'm not sure what you were doing with CAShapeLayer. You'd typically use that if you had a shape that you want to animate around the screen. I suppose you felt that, in scrolling, you might want to do that. But again this is something where you'd want to create the shape layer outside of the drawing path, keep ahold of it, manipulate it outside of the drawing path, then let the system handle worry about putting it on the screen appriopriately.
Your instincts on text are pretty good. You really don't want to handle Text drawing yourself in a system as complex as iOS. There's Unicode issues, glyph substitution, positional forms, ligatures, bi-di text... a whole host of challenges for drawing text on iOS that it's best to leave to things like UILabel. But you want to keep building your view hierarchy separate from drawing in your view hierarchy. drawRect can be called any time even a pixel of your view needs to be redrawn and adding a new subview each time is not the best way to go. In my reworked example, I'm drawing the text using NSString - It's still not the "right" way to do it but it's fairly low level while still giving the framework a chance to do some of the text handling.
In the end you would want to work with the frameworks instead of against them. You'd want to use something like UIScrollView because it will handle a thousand details (bouncing at the boundaries, ease-in/ease-out animation, touch point tracking, fast and slow scrolling, etc) but for a learning experience your code is just fine and I hope you enjoy working with iOS more!

Related

Setting the frame of a transformed CALayer

I'm currently playing with CALayers a bit. For demo purposes I'm creating a custom UIView that renders a fuel gauge. The view has two sub-layers:
one for the background
one for the hand
The layer that represents the hand is then simple rotated accordingly to point at the correct value. So far, so good. Now I want the view to resize its layers whenever the size of the view is changed. To achieve this, I created an override of the layoutSubviews method like this:
public override func layoutSubviews()
{
super.layoutSubviews()
if previousBounds == nil || !previousBounds!.equalTo(self.bounds)
{
previousBounds = self.bounds
self.updateLayers(self.bounds)
}
}
As the method is being called many times, I'm using previousBounds to make sure I only perform the update on the layers when the size has actually changed.
At first, I had just the following code in the updateLayers method to set the frames of the sub-layers:
backgroundLayer.frame = bounds.insetBy(dx: 5, dy: 5)
handLayer.frame = bounds.insetBy(dx: 5, dy: 5)
That worked fine - until the handLayer was rotated. In that case some weird things happen to its size. I suppose it is because the frame gets applied after the rotation and of course, the rotated layer doesn't actually fit the bounds and is thus resized to fit.
My current solution is to temporarily create a new CATransaction that suppresses animations, reverting the transformation back to identity, setting the frame and then re-applying the transformation like this:
CATransaction.begin()
CATransaction.setDisableActions(true)
let oldTransform = scaleLayer.transform
handLayer.transform = CATransform3DIdentity
handLayer.frame = bounds.insetBy(dx: 5, dy: 5)
handLayer.transform = oldTransform
CATransaction.commit()
I already tried omitting the CATransaction and instead applying the handLayer.affineTransform to the bounds I'm setting, but that didn't yield the expected results - maybe I did it wrong (side question: How to rotate a given CGRect around its center without doing all the maths myself)?
My question is simply: Is there a recommended was of setting the frame of a transformed layer or is the solution I found already "the" way to do it?
EDIT
Kevvv provided some sample code, which I've modified to demonstrate my problem:
class ViewController: UIViewController {
let customView = CustomView(frame: .init(origin: .init(x: 200, y: 200), size: .init(width: 200, height: 200)))
let backgroundLayer = CALayer()
let handLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(customView)
customView.backgroundColor = UIColor.blue
backgroundLayer.backgroundColor = UIColor.yellow.cgColor
backgroundLayer.frame = customView.bounds
let handPath = UIBezierPath()
handPath.move(to: backgroundLayer.position)
handPath.addLine(to: .init(x: 0, y: backgroundLayer.position.y))
handLayer.frame = customView.bounds
handLayer.path = handPath.cgPath
handLayer.strokeColor = UIColor.black.cgColor
handLayer.backgroundColor = UIColor.red.cgColor
customView.layer.addSublayer(backgroundLayer)
customView.layer.addSublayer(handLayer)
handLayer.transform = CATransform3DMakeRotation(5, 0, 0, 1)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
customView.addGestureRecognizer(tap)
}
#objc func tapped(_ sender: UITapGestureRecognizer) {
customView.frame = customView.frame.insetBy(dx:10, dy:10)
/*let animation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
let fromValue = self.handLayer.transform
let toValue = CGFloat.pi * 2
animation.duration = 2
animation.fromValue = fromValue
animation.toValue = toValue
animation.valueFunction = CAValueFunction(name: .rotateZ)
self.handLayer.add(animation, forKey: nil)*/
}
}
class CustomView: UIView {
var previousBounds: CGRect!
override func layoutSubviews() {
super.layoutSubviews()
if previousBounds == nil || !previousBounds!.equalTo(self.bounds) {
previousBounds = self.bounds
self.updateLayers(self.bounds)
}
}
func updateLayers(_ bounds: CGRect) {
guard let sublayers = self.layer.sublayers else { return }
for sublayer in sublayers {
sublayer.frame = bounds.insetBy(dx: 5, dy: 5)
}
}
}
If you add this to a playground, then run and tap the control, you'll see what I mean. Watch the red "square".
Do you mind explaining what the "weird things happening to the size" means? I tried to replicate it, but couldn't find the unexpected effects:
class ViewController: UIViewController {
let customView = CustomView(frame: .init(origin: .init(x: 200, y: 200), size: .init(width: 200, height: 200)))
let backgroundLayer = CALayer()
let handLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(customView)
backgroundLayer.backgroundColor = UIColor.yellow.cgColor
backgroundLayer.frame = customView.bounds
let handPath = UIBezierPath()
handPath.move(to: backgroundLayer.position)
handPath.addLine(to: .init(x: 0, y: backgroundLayer.position.y))
handLayer.frame = customView.bounds
handLayer.path = handPath.cgPath
handLayer.strokeColor = UIColor.black.cgColor
customView.layer.addSublayer(backgroundLayer)
customView.layer.addSublayer(handLayer)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
customView.addGestureRecognizer(tap)
}
#objc func tapped(_ sender: UITapGestureRecognizer) {
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
let fromValue = self.handLayer.transform
let toValue = CGFloat.pi * 2
animation.duration = 2
animation.fromValue = fromValue
animation.toValue = toValue
animation.valueFunction = CAValueFunction(name: .rotateZ)
self.handLayer.add(animation, forKey: nil)
}
}
class CustomView: UIView {
var previousBounds: CGRect!
override func layoutSubviews() {
super.layoutSubviews()
if previousBounds == nil || !previousBounds!.equalTo(self.bounds) {
previousBounds = self.bounds
self.updateLayers(self.bounds)
}
}
func updateLayers(_ bounds: CGRect) {
guard let sublayers = self.layer.sublayers else { return }
for sublayer in sublayers {
sublayer.frame = bounds.insetBy(dx: 5, dy: 5)
}
}
}
Edit
I think the issue is that the red box is resized with a frame. Since a frame is always upright even if it's rotated, if you were to do an inset from a frame, it'd look like this:
However, if you were to resize the red box with bounds:
sublayer.bounds = bounds.insetBy(dx: 5, dy: 5)
sublayer.position = self.convert(self.center, from: self.superview)
instead of:
sublayer.frame = bounds.insetBy(dx: 5, dy: 5)
You'll probably have to re-center the handPath and everything else in it accordingly as well.

How can I change a variable value using button/slider/touch action?

I am a student and very new to coding (Xcode with Swift language in this case).
I was trying to change a variable using a button/slider/gesture like this:
There are two variables (firstVariable & secondVariable) and set up a UIslider input that should define the value of the variables
I wanted the variables to be the same value of the slider/touchgesture but the variables remain 4 and 6 even after scrolling the slider in the simulator.
I think its because they are out of the scope of the slider function but how can I link them?
class MyViewController : UIViewController {
var firstVariable = 4
override func viewDidLoad() {
super.viewDidLoad()
var secondVariable = 6
print(secondVariable)
print(firstVariable)
let myslider = UISlider()
//create a UISLider
myslider.maximumValue = 100
myslider.minimumValue = 0
myslider.value = 30
myslider.frame = CGRect(x: 150, y: 200, width: 200, height: 40)
myslider.addTarget(self, action: #selector(sliderinput), for: UIControl.Event.valueChanged)
self.view.addSubview(myslider)
}
#objc func sliderinput(sender: UISlider) {
firstVariable = Int(sender.value)
secondVariable = Int(sender.value)
}
}
Thanks in advance!
First of all, take your secondVariable outside of the viewDidLoad as you cannot access it outside of viewDidLoad. To learn about scope read this. And you need to print the value of the variables to see the values are actually changing.
#objc func sliderinput(sender: UISlider) {
firstVariable = Int(sender.value)
secondVariable = Int(sender.value)
print(secondVariable)
print(firstVariable)
}
Sorry, this is the real context. I wanted to make an infinite timeline like the iOS calendar day tab or like a video editing software timeline for example.
I haven’t yet figured out how to get the distance from the beginning and end of the touch and how to link it to the scrollDistance variable on the beginning so I could create a scroll down effect and later a pinchgesturerecognizer to set the spacing/zoom between the lines.
I know I could use a UIscrollview or UIcollectionview but I felt it was good idea to create a custom one xD
Once again thanks for everyones support! :D
import UIKit
class ViewController: UIViewController {
let lineElements: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let spacing: Int = 30 //spacing between elements
var scrollDistance = 90 // 90 for example, this should be the pangesture distance
//UIPanGestureRecogn function to determine the Y scroll distance from touch began to ended
#objc func touchinput (sender: UIPanGestureRecognizer) {
let touchlocation = sender.location(in: view)
var touchStart: CGPoint
var touchEnd: CGPoint
print("touch current location is", touchlocation)
if sender.state == UIGestureRecognizer.State.began {
touchStart = sender.location(in: view)
print("touch start", touchStart)
} else if sender.state == UIGestureRecognizer.State.ended {
touchEnd = sender.location(in: view)
print("touch end", touchEnd)
}
//I haven’t figure out how to get the distance between start and end and link it to the scrollDistance
//let scrollDistance = touchEnd.y - touchStart.y
}
override func viewDidLoad() {
super.viewDidLoad()
// ===================== Timeline a partir daqui =========================
let totalElements: Int = lineElements.count
for n in 1...totalElements {
let yPosition = lineElements[n - 1] * spacing + scrollDistance //Get Line heigh index times the spacing + scroll distance by touch pan gesture
let hourlabel = UILabel()
let linepath = UIBezierPath()
linepath.move(to: CGPoint(x: 60, y: yPosition))
linepath.addLine(to: CGPoint(x: 350, y: yPosition))
linepath.lineWidth = 8
let lineshape = CAShapeLayer()
lineshape.path = linepath.cgPath
lineshape.strokeColor = UIColor.blue.cgColor
lineshape.fillColor = UIColor.white.cgColor
self.view.layer.addSublayer(lineshape)
hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40)
hourlabel.text = "\(n):00"
hourlabel.font = UIFont(name: "Avenir-Claro", size: 12)
hourlabel.textColor = UIColor.blue
hourlabel.textAlignment = .right
self.view.addSubview(hourlabel)
}
// ===== GESTURE =====
let gesto = UIPanGestureRecognizer()
gesto.addTarget(self, action: #selector(touchinput))
view.addGestureRecognizer(gesto)
}
}
´´´

UIView incorrect frame on #3x devices

I have a really weird bug on #3x devices where a UIView is visually incorrectly sized.
I insist on the visually word because when I print out the frame of that particular view, everything is correct.
On #2x devices, everything looks fine.
My view hierarchy is really simple. I have a view (view B) inside another one (view A).
View B is centered in its superview (view A).
Really simple right?
Frame of view B should be:
CGRect(x: 8.5, y: 15.5, width: 19.0, height: 5.0)
If we scale it with a factor of 3 (to obtain its real dimensions on #3x devices), we should have a rectangle with the following frame:
CGRect(x: 25.5, y: 46.5, width: 57.0, height: 15.0)
When testing on a #3x device, visually, the origin.x of the view is 26px (instead of 25.5), and its width is 56px (instead of 57px).
Here is the code:
class ViewController: UIViewController {
private static let centeredViewSize = CGSize(width: 36, height: 36)
private let centeredView = UIView(frame: .zero)
private let rectangleView = UIView(frame: .zero)
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.rectangleView.clipsToBounds = true
self.rectangleView.backgroundColor = UIColor.purple
self.centeredView.backgroundColor = UIColor.red
self.centeredView.addSubview(self.rectangleView)
self.view.addSubview(self.centeredView)
}
// MARK: - Layout
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.centeredView.frame.size = ViewController.centeredViewSize
self.centeredView.center = self.view.center
let rectangleViewSize = CGSize(width: 19, height: 5)
let rectangleViewHorizontalOrigin = self.centeredView.bounds.midX - (rectangleViewSize.width / 2)
let rectangleViewVerticalOrigin = self.centeredView.bounds.midY - (rectangleViewSize.height / 2)
let rectangleViewOrigin = CGPoint(x: rectangleViewHorizontalOrigin, y: rectangleViewVerticalOrigin)
self.rectangleView.frame = CGRect(origin: rectangleViewOrigin, size: rectangleViewSize)
}
}
This whole issue seems to come from the horizontal origin.
If I round it like this:
let rectangleViewHorizontalOrigin = (self.centeredView.bounds.midX - (rectangleViewSize.width / 2)).rounded()
The issue is gone. But that's not a solution. I want that view to be perfectly centered in its superview.
How can I fix this?
I created a demo project so you can try it out.
I run your code. Your frames are perfectly centred at any device.
Only two things I've changed.
1st in:
override func viewDidLoad() {
super.viewDidLoad()
// Change the order of adding subviews
self.view.addSubview(self.centeredView)
self.centeredView.addSubview(self.rectangleView)
}
2nd:
I use override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
instead override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
}
If something will go wrong, please edit your question with new screenshots.
Here is my console output.

How can I animate a gradient layer which increases its width?

I'm using a simple UIView which I want to animate and I added a gradient layer to it.
I want to increase the width of the view and the layer placed on the view,
but all I get is that the view increases its width but not the layer.
Example: Let's say I have a UIView with height = width = 50
I animate it by setting the width to: width += 50. This animation is working. If I do the same with layer then noting happens. The layer does not increase its width. I tried some things to fix this (see comments in code) but nothing is working.
Here is my code
func performNextTitleAnimation() {
let overlayViewHeight = overlayView.frame.size.height
let overlayViewWidth = overlayView.frame.size.width
let animationHeight: CGFloat = 48
let overlayViewHalfHeight = (overlayViewHeight) / 2
swipeAnimation = UIView(frame: CGRect(x: 0, y: overlayViewHalfHeight - (animationHeight/2), width: 48, height: animationHeight))
swipeAnimation.backgroundColor = .gray
swipeAnimation.layer.cornerRadius = swipeAnimation.frame.size.height / 2
gradientLayer.frame = swipeAnimation.bounds
overlayView.addSubview(swipeAnimation)
swipeAnimation.layer.addSublayer(gradientLayer)
UIView.animate(withDuration: 1.2, delay: 0, options: [.repeat], animations: {
self.gradientLayer.cornerRadius = 24
self.swipeAnimation.frame.size.width += 50
//Things I tried, but not working
//1.) self.gradientLayer.frame.size.width += 50
//2.) self.gradientLayer.frame.size.width = self.swipeAnimation.frame.size.width
//3.) self.gradientLayer.bounds = self.swipeAnimation.bounds
}, completion: nil)
}
Gradient Layer
gradientLayer.colors = [UIColor.white.cgColor, animationColor.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.transform = CATransform3DMakeRotation(3*(CGFloat.pi) / 2, 0, 0, 1)
Any help is highly appreciated.
The best result I've ever achieved when I needed to animate CAGrandientLayers size is to use it as a layerClass of a custom UIView:
class GrandientView: UIView {
override class var layerClass : AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
// it is safe to force cast here
// since we told UIView to use this exact type
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
// setup your gradient
}
}

How to show tooltip on a point click in swift

I am drawing the lines using the following code. The line edges have dots and i want to show the tooltip when user click on the end dots.
The code snippet is below,
override func drawRect(rect: CGRect) {
// Drawing code
UIColor.brownColor().set()
let context = UIGraphicsGetCurrentContext()
//CGContextSetLineWidth(context, 5)
CGContextMoveToPoint(context, 50, 50)
CGContextAddLineToPoint(context,100, 200)
CGContextStrokePath(context)
// now add the circle on at the line edges
var point = CGPoint(x:50 , y:50)
point.x -= 5.0/2
point.y -= 5.0/2
var circle = UIBezierPath(ovalInRect: CGRect(origin: point, size: CGSize(width: 5.0,height: 5.0)))
circle.fill()
point = CGPoint(x:100 , y:200)
point.x -= 5.0/2
point.y -= 5.0/2
circle = UIBezierPath(ovalInRect: CGRect(origin: point, size: CGSize(width: 5.0,height: 5.0)))
circle.fill()
}
It is currently displaying the following image,
and i want to show the tooltip like below,
Does anybody have an idea how will i recognise that this particular dot is clicked and also how will i show the tooltip.
I am looking for a solution in swift.
Define a struct to hold points you have to touch, and the text to show:
struct TouchPoint {
var point: CGPoint // touch near here to show a tooltip
var tip: String // show this text when touched
}
then in the UIView subclass where you define drawRect, make somewhere to keep them:
var touchPoints: [TouchPoint] = [] // where we have to touch and what tooltip to show
drawRect can be called many times, so start fresh each time:
override func drawRect(rect: CGRect) {
touchPoints = []
// ...
// add a touchPoint for every place to touch
touchPoints.append(TouchPoint(point: point, tip: "point 1"))
}
You need to detect taps on the UIView, so add a gesture recognizer by changing its initialisation:
required init?(coder aDecoder: NSCoder) {
// standard init for a UIView wiht an added gesture recognizer
super.init(coder: aDecoder)
addGestureRecognizer(UITapGestureRecognizer(target: self, action: Selector("touched:")))
}
then you need a method to see whether touches are near touchpoints, and to show the right tooltip:
func touched(sender:AnyObject) {
let tapTolerance = CGFloat(20) // how close to the point to touch to see tooltip
let tipOffset = CGVector(dx: 10, dy: -10) // tooltip offset from point
let myTag = 1234 // random number not used elsewhere
guard let tap:CGPoint = (sender as? UITapGestureRecognizer)?.locationInView(self) else { print("touched: failed to find tap"); return }
for v in subviews where v.tag == myTag { v.removeFromSuperview() } // remove existing tooltips
let hitPoints:[TouchPoint] = touchPoints.filter({CGPointDistance($0.point, to: tap) < tapTolerance}) // list of tooltips near to tap
for h in hitPoints { // for each tooltip to show
let f = CGRect(origin: h.point+tipOffset, size: CGSize(width: 100, height: 20)) // fixed size label :-(
let l = UILabel(frame: f)
l.tag = myTag // just to be able to remove the tooltip later
l.text = h.tip // draw the text
addSubview(l) // add the label to the view
}
}
func CGPointDistanceSquared(from: CGPoint, to: CGPoint) -> CGFloat { return (from.x - to.x) * (from.x - to.x) + (from.y - to.y) * (from.y - to.y) }
func CGPointDistance(from: CGPoint, to: CGPoint) -> CGFloat { return sqrt(CGPointDistanceSquared(from, to: to)) }
and that incidentally uses a new version of the + operator to perform vector addition on CGPoint:
func +(left: CGPoint, right: CGVector) -> CGPoint { return CGPoint(x: left.x+right.dx, y: left.y+right.dy) }
and that works OK for me. Extra tweaks would be to compute the UILabel size from the text string, and move the UILabel so it didn't run off the side of the UIView at the edges. Good Luck!