does anybody understand this paradox with swift frames? - swift

In UIKIT I have two uiview's main view and uiview installed with storyboard at top with high in 1/3 of main View.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var TopView: UIView!
#IBOutlet weak var MiddleView: UIView!
#IBOutlet weak var BottomView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let t = Vvp(inView: TopView)
TopView.addSubview(t)
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: TopView.frame.maxX, y: 0))
bezierPath.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
TopView.layer.addSublayer(shapeLayer)
}
}
second view:
func Vvp(inView: UIView)-> UIView {
let viewWithBeizer = UIView(frame: inView.frame)
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: inView.frame.maxX, y: 0))
bezierPath.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
// apply other properties related to the path
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 1.0
viewWithBeizer.layer.addSublayer(shapeLayer)
return viewWithBeizer
}
both views work with the same frame, at storyboard all borders are at zero
why lines are not the same?

The problem has nothing to do with where the lines are being drawn...
The issue is that you are referring to frame when you should be using bounds, and you're setting the frames before auto-layout has configured your views.
Based on your screen-shots, you are laying out your views in Storyboard based on an iPhone model with a Notch... so, in viewDidLoad() your TopView has the frame that was set in Storyboard.
This is how it looks using an iPhone 13 Pro in Storyboard:
As you can see, even though the yellow TopView is constrained to the top of the safe area, its Y position is 44. So, your code in your func Vvp(inView: UIView) is setting the Frame Y-position to 44, instead of Zero.
If you add these 4 lines at the end of viewDidLoad():
TopView.layer.addSublayer(shapeLayer)
// move t (from Vvp(inView: TopView))
// 40-pts to the right
t.frame.origin.x += 40.0
// give it an orange background color
t.backgroundColor = .orange
// allow it to show outside the bounds of TopView
TopView.clipsToBounds = false
// bring TopView to the front of the view hierarchy
view.bringSubviewToFront(TopView)
The output on an iPad Touch 7th Gen looks like this:
as you can see, TopView's subview (the orange view) is much larger than TopView, and is showing up where you told it to: 44-pts from the top of TopView.
To use the code the way you've written it, you need to call that func - along with the shapeLayer code for TopView - later in the controller's lifecycle... such as in viewDidLayoutSubviews(). If you do that, though, you need to remember it will be called multiple times (any time the main view changes, such as on device rotation), so you'll want to make sure you don't repeatedly add new subviews and layers.
Here's a quick modification of your code:
class ViewController: UIViewController {
#IBOutlet weak var TopView: UIView!
#IBOutlet weak var MiddleView: UIView!
#IBOutlet weak var BottomView: UIView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if TopView.subviews.count == 0 {
// we haven't added the subview or shape layer,
// so let's do that here
let t = Vvp(inView: TopView)
TopView.addSubview(t)
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: TopView.frame.maxX, y: 0))
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
TopView.layer.addSublayer(shapeLayer)
}
}
func Vvp(inView: UIView)-> UIView {
let viewWithBeizer = UIView(frame: inView.bounds)
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: inView.bounds.maxX, y: 0))
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
// apply other properties related to the path
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 1.0
viewWithBeizer.layer.addSublayer(shapeLayer)
return viewWithBeizer
}
}
Result (blue line is not visible, because we've added the red line on top of it):
A better approach, though, is to A) use auto-layout constraints, and B) handle your shapeLayer logic inside a custom UIView subclass -- but that's another topic.

I think that this is a bug with iPod touch 7' emulator - with another emulators code works well. Below you can see the code from my question where I add 2px to red line.

Related

Using Swift and CAShapeLayer() with masking, how can I avoid inverting the mask when masked regions intersect?

This question was challenging to word, but explaining the situation further should help.
Using the code below, I'm essentially masking a circle on the screen wherever I tap to reveal what's underneath the black UIView. When I tap, I record the CGPoint in an array to keep track of the tapped locations. For every subsequent tap I make, I remove the black UIView and recreate each tapped point from the array of CGPoints I'm tracking in order to create a new mask that includes all the previous points.
The result is something like this:
I'm sure you can already spot what I'm asking about... How can I avoid the mask inverting wherever the circles intersect? Thanks for your help!
Here's my code for reference:
class MiniGameShadOViewController: UIViewController {
//MARK: - DECLARATIONS
var revealRadius : CGFloat = 50
var tappedAreas : [CGPoint] = []
//Objects
#IBOutlet var shadedRegion: UIView!
//Gesture Recognizers
#IBOutlet var tapToReveal: UITapGestureRecognizer!
//MARK: - VIEW STATES
override func viewDidLoad() {
super.viewDidLoad()
}
//MARK: - USER INTERACTIONS
#IBAction func regionTapped(_ sender: UITapGestureRecognizer) {
let tappedPoint = sender.location(in: view)
tappedAreas.append(tappedPoint) //Hold a list of all previously tapped points
//Clean up old overlays before adding the new one
for subview in shadedRegion.subviews {
if subview.accessibilityIdentifier != "Number" {subview.removeFromSuperview()}
}
//shadedRegion.layer.mask?.removeFromSuperlayer()
createOverlay()
}
//MARK: - FUNCTIONS
func createOverlay(){
//Create the shroud that covers the orbs on the screen
let overlayView = UIView(frame: shadedRegion.bounds)
overlayView.alpha = 1
overlayView.backgroundColor = UIColor.black
overlayView.isUserInteractionEnabled = false
shadedRegion.addSubview(overlayView)
let path = CGMutablePath()
//Create the box that represents the inverse/negative area relative to the circles
path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
//For each point tapped so far, create a circle there
for point in tappedAreas {
path.addArc(center: point, radius: revealRadius, startAngle: 0.0, endAngle: 2.0 * .pi, clockwise: false)
path.closeSubpath() //This is required to prevent all circles from being joined together with lines
}
//Fill each of my circles
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = .evenOdd
//Cut out the circles inside that box
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
}
}
You asked:
how can I avoid inverting the mask when masked regions intersect?
In short, do not use the .evenOdd fill rule.
You have specified a fillRule of .evenOdd. That results in intersections of paths to invert. Here is a red colored view with a mask consisting of a path with two overlapping circular arcs with the .evenOdd rule:
If you use .nonZero (which, coincidentally, is the default fill rule for shape layers), they will not invert each other:
E.g.
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var maskLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.white.cgColor
return shapeLayer
}()
var points: [CGPoint] = [] // this isn't strictly necessary, but just in case you want an array of the points that were tapped
var path = UIBezierPath()
override func viewDidLoad() {
super.viewDidLoad()
imageView.layer.mask = maskLayer
}
#IBAction func handleTapGesture(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: gesture.view)
points.append(point)
path.move(to: point)
path.addArc(withCenter: point, radius: 40, startAngle: 0, endAngle: .pi * 2, clockwise: true)
maskLayer.path = path.cgPath
}
}
Resulting in:

Adding a CAShapeLayer to a subView

I trying to add a CAShapeLayer to a smaller UIView on the main view (single view application). I have the shape Layer animated and reacting to touch gesture to start drawing it. But it is drawn from the center of the main view and I need to be able to move it and constrain it for layout purposes. I have seen another post which seemed to have the same issues but i think i have confused myself more. I have the below code in the viewDidLoad function on the main ViewController.
It is a test project i am playing with to add new functionality to my existing app once I have it working.
I have tried adding a UIView in the MainStoryBoard of the project and adding linking it to the ViewController.swift file (control drag) to create an outlet then adding both shapeLayer & trackLayer to the UIView.
Code below
#IBOutlet weak var gaugeView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let centre = view.center
let circularPath = UIBezierPath(arcCenter: centre, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
// Track Layer Under Gauge
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
//view.layer.addSublayer(trackLayer)
gaugeView.layer.addSublayer(trackLayer)
// Animated Circular Guage
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 10
shapeLayer.lineCap = .round
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
//view.layer.addSublayer(shapeLayer)
gaugeView.layer.addSublayer(shapeLayer)
}
Once I have the app in the simulator it draws and animates the circle when the view.layer.addSubLayer is not commented out but will not add it to the UIView container with the gaugeView.layer.addSubView is in there.
You need to set the center to gaugeView center instead of view.center here,
override func viewDidLoad() {
super.viewDidLoad()
let centre = CGPoint(x: gaugeView.frame.width/2, y: gaugeView.frame.height/2)
...
}
Note: While using Autolayout, viewDidLoad is never the right place to use frame of a subView that is not yet laid out by the autolayout. Best place to get and use the frame is viewDidLayoutSubviews.

How to place SKSpriteNode in front of CAShapeLayer?

I want to display a SKSpriteNode in front of a CAShapeLayer and this is the code that I am using :
import SpriteKit
import GameplayKit
import UIKit
class GameScene: SKScene {
let progressLayer = CAShapeLayer()
override func didMove(to view: SKView) {
let circle = UIView(frame: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
view.addSubview(circle)
view.sendSubview(toBack: circle)
progressLayer.frame = view.bounds
progressLayer.path = progressPath.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = UIColor.green.cgColor
progressLayer.lineWidth = 20.0
let animation2 = CABasicAnimation(keyPath: "strokeEnd")
animation2.fromValue = 0.0
animation2.toValue = 1.0
animation2.duration = 1
animation2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
progressLayer.add(animation2, forKey: "drawLineAnimation")
circle.layer.addSublayer(progressLayer)
var popup = SKSpriteNode(imageNamed: "popupWorking.png")
popup.anchorPoint = CGPoint(x: 0.5, y: 0.5)
popup.position = CGPoint(x: self.size.width/2, y: self.size.height/2)
popup.size = CGSize(width: 400, height: 200)
popup.zPosition = 10
self.addChild(popup)
}
}
But when I run the code, the CAShapeLayer appears in front of the SKSpriteNode, how do I make the SKSpriteNode appear in front of the CAShapeLayer?
You'll need to change your view hierarchy at the view controller level. By default the view controller's view is an SKView. If you want to place a different UIView behind your SKView, you will need to adjust the view hierarchy.
Modify the view controller's view to be a UIView instead of an SKView. Then, add your shape layer's view as a subview to this main view. After that, add your SKView as a subview of the main view.
Your new view hierarchy should look like this:
View Controller
– view (UIView)
- circle view (UIView with your CAShapeLayer as a sublayer)
- spritekit view (SKView)
While it is possible to combine UIKit and SpriteKit in this way, it may be easier to stay within the SpriteKit world and recreate the circle animation using SKSpriteNodes instead of a CAShapeLayer.
Here is a playground that shows one way of doing it. I followed suggestion by #nathan .
Yellow line is drawn using SKShapeNode. The red shadow needs to be animated behind it and hence uses CAShapeLayer. (Note: I needed to mix the two as animating shadow path would be much more complicated in pure Sprite)
import SpriteKit
import PlaygroundSupport
private func invert(_ path: CGMutablePath) -> CGPath {
var rotation = CGAffineTransform(scaleX: 1.0, y: -1.0);
return path.copy(using: &rotation)!;
}
let bounds = CGRect(x: 0, y: 0, width: 400, height: 200)
let uiview = UIView(frame: bounds)
let pathview = UIView(frame: bounds)
let skview = SKView(frame: bounds)
PlaygroundPage.current.liveView = uiview
// Define the path
let path: CGMutablePath = CGMutablePath();
path.move(to: CGPoint(x:0, y:0))
path.addLine(to: CGPoint(x:200, y:100))
// Use CAShapeLayer to draw the red line
var pathLayer: CAShapeLayer = CAShapeLayer()
pathLayer.position = CGPoint(x: uiview.bounds.minX, y: uiview.bounds.minY + 200)
pathLayer.path = invert(path)
pathLayer.strokeColor = UIColor.red.cgColor
pathLayer.fillColor = nil
pathLayer.lineWidth = 10.0
pathLayer.lineJoin = kCALineJoinBevel
pathLayer.lineCap = kCALineCapRound
pathLayer.zPosition = 3;
let strokeAnimation = CABasicAnimation(keyPath: "strokeAnimation")
strokeAnimation.duration = 2;
strokeAnimation.fromValue = 0;
pathview.layer.addSublayer(pathLayer);
pathLayer.add(strokeAnimation, forKey: "strokePath");
uiview.addSubview(pathview)
//Use SKShapeNode to draw the yellow line
let pathShape: SKShapeNode = SKShapeNode(path: path);
pathShape.strokeColor = .white;
pathShape.lineWidth = 2.0;
pathShape.zPosition = 20;
// Create SK Scene
let scene = SKScene(size: CGSize(width: 400, height: 200))
scene.scaleMode = SKSceneScaleMode.aspectFill
scene.size = skview.bounds.size
scene.addChild(pathShape);
scene.backgroundColor = UIColor.clear;
skview.backgroundColor = UIColor.clear
skview.presentScene(scene);
uiview.insertSubview(skview, aboveSubview: pathview)

Using an UIView as a Mask in another UIView on Swift

I am developing an App on xCode with Swift. My screen has an Image of an animal (outlet "backImage"), over this image I have a view (outlet "coverView"), color black, which covers all the screen. So so far you can't see the animal image, because the "coverView" is in front of it. Then I have another view (outlet "maskView") which is smaller and it's over the big view "coverView". What I want is to use this "maskView" as mask and therefor see the "backImage" through it, like a window.
Is there anyone out there able to figure this out?
Here is my screen, I want to see the woman character behind the big gray view through the smaller white view:
You can set the alpha property from your mask view and add in front of the other view, for instance:
let maskView = UIView()
maskView.backgroundColor = UIColor(white: 0, alpha: 0.5) //you can modify this to whatever you need
maskView.frame = CGRect(x: 0, y: 0, width: imageView.frame.width, height: imageView.frame.height)
yourView.addSubview(maskView)
EDIT: Now that you edited your question with an image, now I see what you need, so here is how you can accomplish that.
func setMask(with hole: CGRect, in view: UIView){
// Create a mutable path and add a rectangle that will be h
let mutablePath = CGMutablePath()
mutablePath.addRect(view.bounds)
mutablePath.addRect(hole)
// Create a shape layer and cut out the intersection
let mask = CAShapeLayer()
mask.path = mutablePath
mask.fillRule = kCAFillRuleEvenOdd
// Add the mask to the view
view.layer.mask = mask
}
With this function, all you need is to have a view and create a shape that it's going to be a hole in that view, for instance:
// Create the view (you can also use a view created in the storyboard)
let newView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
newView.backgroundColor = UIColor(white: 0, alpha: 1)
// You can play with these values and find one that fills your need
let rectangularHole = CGRect(x: view.bounds.width*0.3, y: view.bounds.height*0.3, width: view.bounds.width*0.5, height: view.bounds.height*0.5)
// Set the mask in the created view
setMask(with: rectangularHole, in: newView)
Thank you, #Alexandre Lara! You did it!
Here goes the solution:
#IBOutlet weak var windowView: UIView!
#IBOutlet weak var bigCoverView: UIView!
func setMask(with hole: CGRect, in view: UIView){
// Create a mutable path and add a rectangle that will be h
let mutablePath = CGMutablePath()
mutablePath.addRect(view.bounds)
mutablePath.addRect(hole)
// Create a shape layer and cut out the intersection
let mask = CAShapeLayer()
mask.path = mutablePath
mask.fillRule = kCAFillRuleEvenOdd
// Add the mask to the view
view.layer.mask = mask
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let rectangularHole = windowView.frame.integral
// Set the mask in the created view
setMask(with: rectangularHole, in: bigCoverView!)
}

How to change color of Triangle figure using drawRect on Swift

I have made triangle view, called UpTriangleView. It is used in order to show vote. When they are tapped, I want to change their color. I wanna UIColor.grayColor().setStroke() from instance, however I have no idea how to do it. Please tell me how to do it, if you know. Thank you for your kindeness.
class UpTriangleView: UIView {
override func drawRect(rect: CGRect) {
self.backgroundColor = UIColor.clearColor()
// Get Height and Width
let layerHeight = self.layer.frame.height
let layerWidth = self.layer.frame.width
// Create Path
let line = UIBezierPath()
// Draw Points
line.moveToPoint(CGPointMake(0, layerHeight))
line.addLineToPoint(CGPointMake(layerWidth, layerHeight))
line.addLineToPoint(CGPointMake(layerWidth/2, 0))
line.addLineToPoint(CGPointMake(0, layerHeight))
line.closePath()
// Apply Color
UIColor.grayColor().setStroke()
UIColor.grayColor().setFill()
line.lineWidth = 3.0
line.fill()
line.stroke()
// Mask to Path
let shapeLayer = CAShapeLayer()
shapeLayer.path = line.CGPath
self.layer.mask = shapeLayer
}
}
class QATableViewCell : UITableViewCell{
#IBOutlet weak var upTriangleView: UpTriangleView!
}
Add a property to your UpTriangleView that is the color you want to draw it. Implement didSet and call setNeedsDisplay() if the color is set:
class UpTriangleView: UIView {
var color = UIColor.gray {
didSet {
self.setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
self.backgroundColor = .clear
// Get Height and Width
let layerHeight = self.layer.bounds.height
let layerWidth = self.layer.bounds.width
// Create Path
let line = UIBezierPath()
// Draw Points
line.move(to: CGPoint(x: 0, y: layerHeight))
line.addLine(to: CGPoint(x: layerWidth, y: layerHeight))
line.addLine(to: CGPoint(x: layerWidth/2, y: 0))
line.addLine(to: CGPoint(x: 0, y: layerHeight))
line.close()
// Apply Color
color.setStroke()
color.setFill()
line.lineWidth = 3.0
line.fill()
line.stroke()
// Mask to Path
let shapeLayer = CAShapeLayer()
shapeLayer.path = line.cgPath
self.layer.mask = shapeLayer
}
}
Now, demo it in a Playground (see picture below for results):
let utv = UpTriangleView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
utv.color = .yellow // now triangle is yellow
utv.color = .red // now triangle is red
setNeedsDisplay will tell iOS your view needs redrawing, and drawRect will be called again using the newly set color.