CALayer's property isGeometryFlipped doesn't work on macOS - swift

I want to change the NSView's geometry to the top-left corner. But the result isn't correct.( PS: the result is correct on the iOS platform.)
my code like this & the effect is as follows:
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.wantsLayer = true
self.view.layer?.isGeometryFlipped = true
let red = NSView(frame: NSRect(origin: .zero, size: CGSize(width: 400, height: 200)))
red.wantsLayer = true
red.layer?.backgroundColor = NSColor.red.cgColor
let yellow = NSView(frame: NSRect(origin: .zero, size: CGSize(width: 200, height: 100)))
yellow.wantsLayer = true
yellow.layer?.backgroundColor = NSColor.yellow.cgColor
red.addSubview(yellow)
view.addSubview(red)
}
}
Why I set the property isGeometryFlipped= true of View's layer, the geometry isn't change
from bottom-left corner to the top-left corner?

It's because coordinate system in iOS and macOS are not the same.
Point (x:0, y:0) in :
- iOS : at the top left
macOS : at the bottom left
if you want the yellow view at the top left of the red view,just do this:
let yellow = NSView(frame: NSRect(origin: CGPoint(x: 0, y: 400), size: CGSize(width: 200, height: 100)))

Related

Is SKCropNode limited to a width below 5000?

I am trying to use a SKCropNode with a width of at least 5000 pixels, yet if the maskNode property width is greater than 4100 something strange happens -- the first 100-200 or so pixels are not cropped, and the cropping spans approx only 4100 pixels.
I'm on a Mac Mini M1, Ventura 13.0.1 and Xcode 14.1 (14B47b)
Here is the working code:
import SwiftUI
import SpriteKit
struct ContentView: View {
#State var spriteView : SpriteView!
var body: some View {
ScrollView(.horizontal, showsIndicators: true) {
ZStack {
if spriteView != nil {
spriteView
}
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
}
.padding()
.onAppear {
spriteView = .init(scene: myScene)
}
.frame(minWidth: 1000, minHeight: 500)
}
}
var myScene : SKScene {
let r = SKScene.init(size: .init(width: 2000, height: 500))
r.backgroundColor = .yellow
let maskRect = SKShapeNode.init(rect: .init(x: 0, y: 100, width: 5000, height: 300))
// let maskRect = SKShapeNode.init(rect: .init(x: 0, y: 100, width: 4096+2, height: 300))
maskRect.fillColor = .white
maskRect.lineWidth = 0
let redRect = SKShapeNode.init(rect: .init(origin: .zero, size: .init(width: 5000, height: 500)))
redRect.fillColor = .red
redRect.lineWidth = 0
let path = CGMutablePath.init()
path.move(to: .zero)
path.addLine(to: .init(x: 1000, y: 500))
let blueLine = SKShapeNode.init(path: path)
blueLine.strokeColor = .blue
blueLine.lineWidth = 12
redRect.addChild(blueLine)
let cropNode = SKCropNode.init()
cropNode.maskNode = SKNode.init()
cropNode.maskNode?.addChild(maskRect)
cropNode.addChild(redRect)
let blackRect = SKShapeNode.init(rect: .init(origin: .init(x: 0, y: 100), size: .init(width: 5000, height: 300)))
blackRect.fillColor = .black
blackRect.lineWidth = 0
r.addChild(blackRect)
r.addChild(cropNode)
return r
}
}
The result I get is
If I change the width to 4096 I get the correct result without the black rectangle on the left hand side, ie let maskRect = SKShapeNode.init(rect: .init(x: 0, y: 100, width: 4096, height: 300))
EDIT:
Using SKSpriteNode as suggested produced the same results for me:
let maskRect = SKSpriteNode.init(color: .black, size: .init(width: 5000, height: 300))
maskRect.position.x = 0 + maskRect.size.width/2
maskRect.position.y = 100 + maskRect.size.height/2
this appears to be an issue with SKShapeNode.
solution: use SKSpriteNode instead -- both for your mask and for any other large elements in the scene. here is an illustration (swap the red and green nodes to see the glitch):
import SwiftUI
import SpriteKit
struct GameSceneCrop: View {
#State private var scene = CropScene()
var body: some View {
SpriteView(scene: scene)
}
}
class CropScene: SKScene {
let skCameraNode = SKCameraNode()
var WIDTH:CGFloat = 5000
var HEIGHT:CGFloat = 1000
let container = SKNode()
override func didMove(to view: SKView) {
scaleMode = .aspectFill // Set the scale mode to scale to fit the window
backgroundColor = .yellow
anchorPoint = CGPoint(x: 0.5, y: 0.5)
//set up camera node
camera = skCameraNode
addChild(skCameraNode)
skCameraNode.setScale( 10000 )
addChild(container)
rebuild()
}
func rebuild() {
container.removeAllChildren()
//a green SKSpriteNode
let green = SKSpriteNode(color: .green, size: CGSize(width: WIDTH, height: HEIGHT))
//a red SKShapeNode
let red = SKShapeNode(rectOf: CGSize(width: WIDTH, height: HEIGHT))
red.fillColor = .red
//crop node
let mask = SKSpriteNode(color: .black, size: red.frame.size) //using SKSpriteNode here instead of SKShapeNode
let crop_node = SKCropNode()
crop_node.maskNode = mask
crop_node.addChild(green) //<--- replace green SKSpriteNode with red SKShapeNode to see clipping glitch
container.addChild(crop_node)
//draw outline around red block
let reference_frame = SKShapeNode(rect: red.frame.insetBy(dx: -50, dy: -50))
reference_frame.strokeColor = .black
reference_frame.lineWidth = 50
container.addChild(reference_frame)
}
override func update(_ currentTime: TimeInterval) {
let x = 0.65 + (sin(currentTime*2) + 1) / 4
WIDTH = 5000 * x
rebuild()
}
}

How do I draw a rectangle with equal line widths into an NSStatusItemButton?

The following code draws a rectangle with NSBezierPath into an NSImage, which is then set as the image for an NSStatusItemButton.
import Cocoa
class ViewController: NSViewController {
let statusItem: NSStatusItem = NSStatusBar.system.statusItem(
withLength: NSStatusItem.squareLength)
override func viewDidLoad() {
super.viewDidLoad()
let imageSize = NSSize.init(width: 18.0, height: 18.0)
let statusItemImage = NSImage(
size: imageSize,
flipped: false,
drawingHandler: { (dstRect: NSRect) -> Bool in
NSColor.black.setStroke()
let path = NSBezierPath()
path.appendRect(NSRect(
x: NSMinX(dstRect),
y: NSMinY(dstRect),
width: 10,
height: 10))
path.stroke()
return true
})
statusItem.button?.image = statusItemImage
}
override var representedObject: Any? {
didSet {
}
}
}
In the menu bar, it looks like this:
The left and bottom edge of the rectangle have a different width than the right and top edge.
How do I get a rectangle with equal line widths?
You have to set a lineWidth for your path:
/// The new rectangle
let new = CGRect(origin: dstRect.origin,
size: .init(width: 10, height: 10))
let path = NSBezierPath(rect: new)
// The line width size
path.lineWidth = 0.1
path.stroke()

Central align subView

I'm trying to centre a UIView in another view (so like a pop up view), but whatever I do, I just cannot align it centrally!
func loadPopUpView() {
let customView = CGRect(x: view.center.x, y: view.center.y, width: 100, height: 100)
popUpView = UIView(frame: customView)
popUpView.backgroundColor = UIColor.black
view.addSubview(popUpView)
popUpView.isHidden = false
}
I've changed the background colour to black just so I know when it appears.
I cannot do this with storyboard because it's going on a tableView, so I'm trying to do it programmatically. Result Image
You also need to minus half value of your custom view height and width from x and y like below:
let customView = CGRect(x: view.center.x - 50, y: view.center.y - 50, width: 100, height: 100)
You are telling it to put top left corner in the center, hence you need to let it get half size in position.
let customView = CGRect(x: view.center.x-50, y: view.center.y-50, width: 100, height: 100)
Another solution is the use anchorpoints.
customView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
customView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
customView.heightAnchor.constraint(equalToConstant: 100).isActive = true
customView.widthAnchor.constraint(equalToConstant: 100).isActive = true
On CGRect(x:, y:, width:, height:), the point (x,y) is the origin. In iOS, that's the top left point.
On the CGRect doc:
In the default Core Graphics coordinate space, the origin is located
in the lower-left corner of the rectangle and the rectangle extends
towards the upper-right corner. If the context has a
flipped-coordinate space—often the case on iOS—the origin is in the
upper-left corner and the rectangle extends towards the lower-right
corner.
So to fix this:
let width = 100.0
let height = 100.0
let customViewFrame = CGRect(x: view.center.x - width/2.0, y: view.center.y - height/2.0, width: width, height: height)
Another solution would be to apply the center once the frame (especially the width/size) have been set.
let customViewFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
customViewFrame = view.center

How to apply sharp corner radius with border on UIView in swift?

I want to set corner radius on bottom left and right side with border on UIView. here is code. **issue is it's not display SHARP in 1.5 border **
class BorderView: UIView {
override func draw(_ rect: CGRect) {
let color = UIColor.red
let color2 = UIColor.brown
//// Rectangle Drawing
let rectanglePath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 200, height: 200), byRoundingCorners: [.bottomRight, .bottomLeft], cornerRadii: CGSize(width: 10, height: 10))
rectanglePath.close()
color2.setFill()
rectanglePath.fill()
color.set()
rectanglePath.lineWidth = 1.5
rectanglePath.stroke()
}
}
OutPut
expected output
If you use the border and corner properties on CALayer, it should do it all for you.
// Probably inside init
layer.borderWidth = 1.5
layer.borderColor = UIColor.red.cgColor
layer.cornerRadius = 10
layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]

NSWindow scaled drawing in contentView

I'm creating a bare-bones window programmatically and I just need to draw some lines in its content view. The size of the area the lines fall in (worldBounds) needs to be scaled down and I assume I need the contentView bounds origin to be the same as worldBounds origin so it can draw everything. The approach is to set the frame to 1/8 the size, then use setBoundsOrigin and setBoundsSize on the contentView, which I understand scales the coordinate system.
The problem is no drawing appears. The window appears with the correct size. I have a function drawTestLines set up just to test it. If I remove the calls to setBounds, everything draws fine, but of course then the bounds don't match worldBounds. Any help is much appreciated!
var worldBounds = NSRect()
let scale: CGFloat = 0.125
func drawMap() {
boundLineStore(lineStore, &worldBounds)
worldBounds.origin.x -= 8
worldBounds.origin.y -= 8
worldBounds.size.width += 16
worldBounds.size.height += 16
if !draw {
return
}
let scaled = NSRect(x: 300.0, // to set the window size/position
y: 80,
width: worldBounds.size.width*scale, // 575
height: worldBounds.size.height*scale) // 355
window = NSWindow(contentRect: scaled,
styleMask: .titled,
backing: .buffered,
defer: false)
window.display()
window.orderFront(nil)
window.contentView!.setBoundsSize(worldBounds.size) // (4593, 2833)
window.contentView!.setBoundsOrigin(worldBounds.origin) // (-776, -4872)
// Draw map lines
window.contentView!.lockFocus()
drawTestLines(in: window.contentView!)
window.contentView!.unlockFocus()
}
// for testing
func drawTestLines(in view: NSView) {
let bottom = view.bounds.minY
let top = view.bounds.maxY
let left = view.bounds.minX
let right = view.bounds.maxX
NSColor.black.setStroke()
for i in Int(left)..<Int(right) { // draw vertical lines across the view just to see if it works!
if i%64 == 0 {
NSBezierPath.strokeLine(from: NSPoint(x: CGFloat(i), y: bottom), to: NSPoint(x: CGFloat(i), y: top))
}
}
NSBezierPath.strokeLine(from: NSPoint(x: left, y: bottom), to: NSPoint(x: right, y: top))
NSBezierPath.strokeLine(from: NSPoint.zero, to: NSPoint(x: 50.0, y: 50.0))
}
Experiment: Create a new Xcode project. Change applicationDidFinishLaunching:
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let contentBounds = window.contentView?.bounds {
let scale: CGFloat = 0.5
let worldBounds = CGRect(x: 0.0, y: 0.0, width: contentBounds.size.width * scale, height: contentBounds.size.height * scale)
window.contentView!.bounds = worldBounds
}
}
Change the class of the content view of the window in IB to MyView. Add a new class MyView, subclass of NSView. Change draw to:
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let bottom = self.bounds.minY
let top = self.bounds.maxY
let left = self.bounds.minX
let right = self.bounds.maxX
NSColor.black.setStroke()
for i in Int(left)..<Int(right) { // draw vertical lines across the view just to see if it works!
if i%64 == 0 {
NSBezierPath.strokeLine(from: NSPoint(x: CGFloat(i), y: bottom), to: NSPoint(x: CGFloat(i), y: top))
}
}
NSBezierPath.strokeLine(from: NSPoint(x: left, y: bottom), to: NSPoint(x: right, y: top))
NSBezierPath.strokeLine(from: NSPoint.zero, to: NSPoint(x: 50.0, y: 50.0))
}
See what happens when you change scale or the origin of worldBounds.