How would I make a simple spinning dash in Swift? that spins on its center like a loader in terminal - swift

I'm trying to create a simple spinning loading dash. I know how to do the loop but I can't seem to make it on a single line. Any ideas?
let loop = 1
while loop > 0 {
// spinning dash
}

I will not provide you with all the code to your question but rather a guideline of what to do. In general, its a two step algorithm.
Draw a line
Perform a 360° rotation of it for a desired time, t
The code posted below implements the first portion. I have added comments and I believe it should be self explanatory. After reviewing it, I'd recommend you read about UIBezierPath.
As for the second part, there are two ways of going about this.
1. Rotate the line itself (recommended)
Should you choose this method, here's a tutorial from Ray Wenderlich which covers it extensively along with the Math behind it. Follow through both portions of the tutorial if possible.
2. Rotate the view encompassing the line
Changing the outer view's background color to clear then rotating itself will give the illusion that the line inside is the one rotated. Here's a video guide for view rotations.
import UIKit
class ViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
// This is the black subview in which the line will be drawn into
let lineView: GeneralDraw = GeneralDraw(frame: CGRect(origin: CGPoint(x: 20, y: 30), size: CGSize(width: 300, height: 300)))
// uncomment this to remove the black colour
// lineView.backgroundColor = .clear
// add this lineView to the mainView
self.view.addSubview(lineView)
}
}
// This handles the drawing inside a given view
class GeneralDraw: UIView
{
override init(frame: CGRect)
{
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect)
{
let linePath = UIBezierPath()
// start point of the line
linePath.move(to: CGPoint(x:50, y:10))
// end point of the line
linePath.addLine(to: CGPoint(x:200, y:10))
linePath.close()
// cosmetic settings for the line
UIColor.red.set()
linePath.stroke()
linePath.fill()
}
}

I would use a CAReplicatorLayer for this. You start with a layer that draws a horizontal bar and combine it with transforms that show the bar in the other positions. Then you animate the fading out of the bar, with an offset coordinated to the fading.
In this gif, I've deliberately slowed down the animation. (There is a mild glitch at the point where the gif repeats, but ignore that; the real project doesn't have that glitch.)

1. Solution: rotate Images
create a set of images which shows the dash rotating.
set the images to an array. then animate that `UIImageView.startAnimating()
see section "Animating a Sequence of Images" of UIImageView.
2. Solution: standard iOS activity indicator
But better go with the standard UIActivityIndicatorView
see also:
iOS Human Interface Guidelines: Progress Indicators
Reference for UIActivityIndicatorView

Related

Core Graphics with DisplayLink Unexpected Behavior

I'm trying to learn Core Graphics and am having trouble understanding the behavior of the code I've written, which uses a subclassed UIView and an override of the draw(_ rect:) function.
I've written a basic bouncing ball demo. Any number of random balls are created with random position and speed. They then bounce around the screen.
My issue is the way that the balls appear to move is unexpected and there is a lot of flicker. Here is the sequence inside for loops to iterate through all balls:
Check for collisions.
If there is a collision with the wall, multiply speed by -1.
Increment ball position by ball speed.
I'm currently not clearing the context, so I would expect the existing balls to stay put. Instead they seem to slide smoothly along with the ball that's moving.
I'd like to understand why this is the case.
Here is an image of how the current code runs at 4 fps so that you can see how the shapes are being drawn and shift back and forth:
Here is my code:
class ViewController: UIViewController {
let myView = MyView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
NSLayoutConstraint.activate([
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
myView.widthAnchor.constraint(equalTo: view.widthAnchor),
myView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
createDisplayLink(fps: 60)
}
func createDisplayLink(fps: Int) {
let displaylink = CADisplayLink(target: self,
selector: #selector(step))
displaylink.preferredFramesPerSecond = fps
displaylink.add(to: .current,
forMode: RunLoop.Mode.default)
}
#objc func step(displaylink: CADisplayLink) {
myView.setNeedsDisplay()
}
}
class MyView: UIView {
let numBalls = 5
var balls = [Ball]()
override init(frame:CGRect) {
super.init(frame:frame)
for _ in 0..<numBalls {
balls.append(
Ball(
ballPosition: Vec2(x: CGFloat.random(in: 0...UIScreen.main.bounds.width), y: CGFloat.random(in: 0...UIScreen.main.bounds.height)),
ballSpeed: Vec2(x: CGFloat.random(in: 0.5...2), y: CGFloat.random(in: 0.5...2))))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
for i in 0..<numBalls {
if balls[i].ballPosition.x > self.bounds.width - balls[i].ballSize || balls[i].ballPosition.x < 0 {
balls[i].ballSpeed.x *= -1
}
balls[i].ballPosition.x += balls[i].ballSpeed.x
if balls[i].ballPosition.y > self.bounds.height - balls[i].ballSize || balls[i].ballPosition.y < 0 {
balls[i].ballSpeed.y *= -1
}
balls[i].ballPosition.y += balls[i].ballSpeed.y
}
for i in 0..<numBalls {
context.setFillColor(UIColor.red.cgColor)
context.setStrokeColor(UIColor.green.cgColor)
context.setLineWidth(0)
let rectangle = CGRect(x: balls[i].ballPosition.x, y: balls[i].ballPosition.y, width: balls[i].ballSize, height: balls[i].ballSize)
context.addEllipse(in: rectangle)
context.drawPath(using: .fillStroke)
}
}
}
There are a lot of misunderstandings here, so I'll try to take them one by one:
CADisplayLink does not promise it will call your step method every 1/60 of a second. There's a reason the property is called preferred frames per second. It's just a hint to the system of what you'd like. It may call you less often, and in any case there will be some amount of error.
To perform your own animations by hand, you need to look at what time is actually attached to the given frame, and use that to determine where things are. The CADisplayLink includes a timestamp to let you know that. You can't just increment by speed. You need to multiply speed by actual time to determine distance.
"I'm currently not clearing the context, so I would expect the existing balls to stay put." Every time draw(rect:) is called, you receive a fresh context. It is your responsibility to draw everything for the current frame. There is no persistence between frames. (Core Animation generally provides those kinds of features by efficiently composing CALayers together; but you've chosen to use Core Graphics, and there you need to draw everything every time. We generally do not use Core Graphics this way.)
myView.setNeedsDisplay() does not mean "draw this frame right now." It means "the next time you're going to draw, this view needs to be redrawn." Depending on exactly when the CADisplayLink fires, you may drop a frame, or you might not. Using Core Graphics, you would need to update all the circle's locations before calling setNeedsDisplay(). Then draw(rect:) should just draw them, not compute what they are. (CADisplayLink is really designed to work with CALayers, though, and NSView drawing isn't designed to be updated so often, so this still may be a little tricky to keep smooth.)
The more normal way to create this system would be to generate a CAShapeLayer for each ball and position them on the NSView's layer. Then in the CADisplayLink callback, you would adjust their positions based on the timestamp of the next frame. Alternately, you could just set up a repeating NSTimer or DispatchTimerSource (rather than a CADisplayLink) at something well below the screen refresh speed (like 1/20 s) and move the layer positions in that callback. This would be nice and simple and avoid the complexities of CADisplayLink (which is much more powerful, but expects you to use the timestamp and consider other soft real-time concerns).

Rotating UIControl with CAGradientLayer not updating correctly Swift

Rather than using a normal button, I subclassed a UIControl because I needed to add a gradient to it. I also have a way to add a shadow and an activity indicator (not visible in the image below) as a stateful button to stop users hammering the button if (for example) an API call is being made.
It was really tricky to try to get the UIControl to rotate, and to be able to do this I added the shadow as a separate view to a container view containing the UIControl so a shadow could be added.
Now the issue is the control does not behave quite like a view on rotation - let me show you a screen grab for context:
This is mid-rotation but is just about visible to the eye - the image shows that the Gradient is 75% of the length of a blue UIView in the image.
https://github.com/stevencurtis/statefulbutton
In order to perform this rotation I remove the shadowview and then change the frame of the gradient frame to its bounds, and this is the problem.
func viewRotated() {
CATransaction.setDisableActions(true)
shadowView!.removeFromSuperview()
shadowView!.frame = self.frame
shadowView!.layer.masksToBounds = false
shadowView!.layer.shadowOffset = CGSize(width: 0, height: 3)
shadowView!.layer.shadowRadius = 3
shadowView!.layer.shadowOpacity = 0.3
shadowView!.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 20, height: 20)).cgPath
shadowView!.layer.shouldRasterize = true
shadowView!.layer.rasterizationScale = UIScreen.main.scale
self.gradientViewLayer.frame = self.bounds
self.selectedViewLayer.frame = self.bounds
CATransaction.commit()
self.insertSubview(shadowView!, at: 0)
}
So this rotation method is called through the parent view controller:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { context in
context.viewController(forKey: UITransitionContextViewControllerKey.from)
//inform the loginButton that it is being rotated
self.loginButton.viewRotated()
}, completion: { context in
// can call here when completed the transition
})
}
I know this is the problem, and I guess it is not happening at quite the right time to act the same way as a UIView. Now the issue is that I have tried many things to get this to work, and my best solution (above) is not quite there.
It isn't helpful to suggest to use a UIButton, to use an image for the gradient (please don't suggest using a gradient image as a background for a UIButton, I've tried this) or a third party library. This is my work, it functions but does not work acceptably to me and I want to get it to work as well as a usual view (or at least know why not). I have tried the other solutions above as well, and have gone for my own UIControl. I know I can lock the view if there is an API call, or use other ways to stop the user pressing the button too many times. I'm trying to fix my solution, not invent ways of getting around this issue with CAGradientLayer.
The problem: I need to make a UIControlView with a CAGradientLayer as a background rotate in the same way as a UIView, and not exhibit the issue shown in the image above.
Full Example:
https://github.com/stevencurtis/statefulbutton
Here is working code:
https://gist.github.com/alldne/22d340b36613ae5870b3472fa1c64654
These are my recommendations to your code:
1. A proper place for setting size and the position of sublayers
The size of a view, namely your button, is determined after the layout is done. What you should do is just to set the proper size of sublayers after the layout. So I recommend you to set the size and position of the gradient sublayers in layoutSubviews.
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)
selectedViewLayer.bounds = self.bounds
selectedViewLayer.position = center
gradientViewLayer.bounds = self.bounds
gradientViewLayer.position = center
}
2. You don’t need to use an extra view to draw shadow
Remove shadowView and just set the layer properties:
layer.shadowOffset = CGSize(width: 0, height: 3)
layer.shadowRadius = 3
layer.shadowOpacity = 0.3
layer.shadowColor = UIColor.black.cgColor
clipsToBounds = false
If you have to use an extra view to draw shadow, then you can add the view once in init() and set the proper size and position in layoutSubviews or you can just programmatically set auto layout constraints to the superview.
3. Animation duration & timing function
After setting proper sizes, your animation of the gradient layers and the container view doesn’t sync well.
It seems that:
During the rotation transition, coordinator(UIViewControllerTransitionCoordinator) has its own transition duration and easing function.
And the duration and easing function are applied automatically to all the subviews (UIView).
However, those values are not applied to the CALayer without an associated UIView. Consequently, it uses the default timing function and duration of CoreAnimation.
To sync the animations, explicitly set the animation duration and the timing function like below:
class ViewController: UIViewController {
...
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
CATransaction.setAnimationDuration(coordinator.transitionDuration)
CATransaction.setAnimationTimingFunction(coordinator.completionCurve.timingFunction)
}
...
}
// Swift 4
extension UIView.AnimationCurve {
var timingFunction: CAMediaTimingFunction {
let functionName: CAMediaTimingFunctionName
switch self {
case .easeIn:
functionName = kCAMediaTimingFunctionEaseIn as CAMediaTimingFunctionName
case .easeInOut:
functionName = kCAMediaTimingFunctionEaseInEaseOut as CAMediaTimingFunctionName
case .easeOut:
functionName = kCAMediaTimingFunctionEaseOut as CAMediaTimingFunctionName
case .linear:
functionName = kCAMediaTimingFunctionLinear as CAMediaTimingFunctionName
}
return CAMediaTimingFunction(name: functionName as String)
}
}

Swift: NSBezier

This is the code inside my custom view class:
func drawTestingPoint(_ point: CGPoint, target: Int, output: Int) {
let path = NSBezierPath()
path.appendArc(withCenter: point, radius: 5, startAngle: 0, endAngle: 360)
NSColor.black.setStroke()
if target == output {
NSColor.green.setFill()
} else {
NSColor.red.setFill()
}
path.lineWidth = 3
path.fill()
path.stroke()
}
override func draw(_ dirtyRect: NSRect) {
//If I call the drawTestingPoint function here it works
}
Inside my viewDidLoad method in my NSViewController class I set up the custom view and try to draw the testing point:
let size = getDataViewSize()
let origin = CGPoint(x: view.frame.width/2-size.width/2, y: view.frame.height/2-size.height/2)
dataView = DataView(frame: CGRect(origin: origin, size: size))
view.addSubview(dataView)
dataView.drawTestingPoint(CGPoint(x: view.frame.width/2 y: view.frame.height/2), target: target, output: output)
dataView.needsDisplay = true
My problem is that no point is getting drawn. I think there can't be anything wrong with my drawTestingPoint function because when I call it inside my draw(_ dirtyRect: NSRect) function in my custom NSView class, it works. What can I do so I can call this function inside my viewDidLoad function how you can see in the codes snippets above so my point gets drawn
You can't just draw any time you want. Normally you set up a view and implement draw(_:) as you've done. The system calls the draw method when it needs the view to draw its contents. Before calling your draw(_:) method it sets up the drawing context correctly to draw inside your view and clip if you draw outside of the view. That's the bit you're missing.
As a general rule you should NOT draw outside of the view's draw(_:) method. I've done drawing outside of the draw(_:) method so infrequently that I don't remember what you'd need to do to set up the drawing context correctly. (To be fair I do mostly iOS development these days and my MacOS is getting rusty.)
So the short answer is "Don't do that."
EDIT:
Instead, set up your custom view to save the information it needs to draw itself. As others have suggested, when you make changes to the view, set needsDisplay=true on the view. That will cause the system to call the view's draw(_:) method on the next pass through the event loop

swift text.draw method messes up CGContext

I am using CoreGraphics to draw single glyphs alongside with primitives in a CGContext. The following code works in a swift playground in XCode 9.2 When started in the playground a small rectangle with twice the letter A should appear at the given coordinates in the playground liveView.
import Cocoa
import PlaygroundSupport
class MyView: NSView {
init(inFrame: CGRect) {
super.init(frame: inFrame)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
// setup context properties
let context: CGContext = NSGraphicsContext.current!.cgContext
context.setStrokeColor(CGColor.black)
context.setTextDrawingMode(.fill)
context.setFillColor(CGColor(red: 0.99, green: 0.99, blue: 0.85, alpha: 1))
context.beginPath()
context.addRect(rect)
context.fillPath()
context.setFillColor(.black)
// prepare variables and constants
var font = CTFontCreateWithName("Helvetica" as CFString, 48, nil)
var glyph = CTFontGetGlyphWithName(font, "A" as CFString)
var glyph1Position = CGPoint(x: self.frame.minX, y: self.frame.maxY/2)
var glyph2Position = CGPoint(x: self.frame.minX+150, y: self.frame.maxY/2)
let text = "Hello"
var textOrigin = NSPoint(x: self.frame.minX+50, y: self.frame.maxY/2)
// draw one character
CTFontDrawGlyphs(font, &glyph, &glyph1Position, 1, context)
// *** *** when the next line is uncommented the bug appears *** ***
// text.draw(at: textOrigin)
CTFontDrawGlyphs(font, &glyph, &glyph2Position, 1, context)
}
}
var frameRect = CGRect(x: 0, y: 0, width: 200, height: 100)
PlaygroundPage.current.liveView = MyView(inFrame: frameRect)
Now I want to draw regular text in the same context. However, when the text string is drawn between the drawing of the two glyphs via its own drawing method, the current context seems to be messed up, the second glyph will not show. When the text is drawn after both single glyphs are drawn everything is fine.
So obviously drawing the text seems to have an impact on the current CGContext, but I cannot find out what exactly is happening. I tried the saveGstate() method befor drawing the string and restoring afterwards, but without success.
I also tried using CoreText methods to create an attributed String with CTFramesetterCreateWithAttributedString and showing it with CTFramesetterCreateFrame, it also does not work, here after the creation of the framesetter the context is messed up.
My actual playground is more complex, there the glyphs do not entirely disappear but are shown at a wrong vertical position, but the basic problem - and question is the same:
How can I draw the text into the currentContext whithout any other changes to the context being done in the background?
You need to set the text matrix (the transform applied to text drawing). You should always set this before drawing text, because it isn't part of the graphics state and may get trashed by other drawing code such as AppKit's string drawing extensions.
Add the following before your call to CTFontDrawGlyphs:
context.textMatrix = .identity
This should be called in the initial setup of the context since there is no promise that the text matrix will be identity before calling drawRect. Then, any time you have made calls to something that modifies the text matrix you will need to set it back to what you want (identity in this case, but it could be something else if you wanted to draw in a fancy way).
It is not always obvious what will modify the text matrix. AppKit drawing functions almost always do (though I'm not aware of any documentation indicating this). Core Text functions that modify the context, like CTFrameDraw and CTLineDraw, will generally document this fact with a statement such as:
This call can leave the context in any state and does not flush it after the draw operation.
Similarly CTFontDrawGlyphs warns:
This function modifies graphics state including font, text size, and text matrix if these attributes are specified in font. These attributes are not restored.
As a rule I discourage mixing text drawing systems. Use AppKit or Core Text, but don't mix them. If you pick one and stick to it, then this generally isn't a problem (as long as you initialize the matrix once at the top of drawRect). For example, if you did all the drawing with CTFontDrawGlyphs, you wouldn't need to reset the matrix each time; you'd stay in the Core Text matrix and it'd be fine (which is why this works when you comment out the draw(at:) call).

Two color items on Tab Bar, Swift

Is is possible to set an item with two colors on the tab bar?
(I'm a beginner in swift)
What I want to achieve:
I want to have something-like a white background for each item in both active and inactive states.
-> Example of what I want
What I have tried so far:
I've been trying by using a png with transparency, white color and black color but it seems that anyhing that is not a transparent pixel is taken as the same color. (The asset I've working with is rendered as a whole color image instead of differentiating black and white)
-> The asset I thought it would work
What I think its the way but don't know how:
(This is an assumption)
I think this could be done by setting an inactive item in the background (first layer, white color) and an active item (second layer, black color) in the same coordinates with the color I want to change:
-> Two assets for Layer 1 and layer 2
I'm using an storyboard with storyboard references to set initial configuration for the assets of both active an inactive states.
I'm setting active state color programatically.
Suggestions to achive this in more elegant ways are welcome.
Thank you :)
Thank you all for your help.
Inspired by #Jake link, I kept searching and found that Xcode has different modes to render image assets.
Render as Template:
The default one, renders images as they were templates. (You can take any image and colorize it programatically, but it does not recognice if it has more than 1 color)
Render as Original:
The other mode, renders images as they are. So it actually recognizes if it has more than 1 color and leave them that way. This allows you to have tab bar items with multiple colors.
How can this property be modified?
Programatically:
(as a property)
tabBarItem.image = tabBarImage.withRenderingMode(.alwaysOriginal)
//or
tabBarItem.image = tabBarImage.withRenderingMode(.alwaysTemplate)
Via Interface Builder:
1. Assets Folder, 2. Select an asset, 3. Attribute Inspector
Render as: Default, Original Image, Template Image.
Subclass your TabbarviewController, and set the color:
tabBar.tintColor = .red
I would probably just draw this in quartz. It's a very simple vector and would be easy to do in few lines of code. It would also react to a change of state and update accordingly. See below for an example UIView that does exactly this:
class TabIcon: UIView {
var enabled: Bool = false {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let ctx = UIGraphicsGetCurrentContext()
ctx?.setFillColor(UIColor.white.cgColor)
ctx?.addEllipse(in: rect)
ctx?.fillPath()
ctx?.setFillColor(enabled ? UIColor.purple.cgColor : UIColor.lightGray.cgColor)
ctx?.addEllipse(in: CGRect(x: rect.minX + 5, y: rect.minY + 5, width: rect.width - 10, height: rect.height - 10))
ctx?.fillPath()
ctx?.setFillColor(UIColor.white.cgColor)
ctx?.addEllipse(in: CGRect(x: rect.minX + 15, y: rect.minY + 15, width: rect.width - 30, height: rect.height - 30))
ctx?.fillPath()
}
}
All you have to do is use this view as the tab icon, changing the enabled property when it is selected will automatically trigger a redraw. The result looks like this:
Of course, fine tune the values I have used to match your design exactly. Also note I've hardcoded values, you could determine your frames based on a proportion of the overall size for better reusability.
Hope this helps!