Round Specific Corners in a SwiftUI Mac App - macos

I've been trying to figure out how to round specific corners of a SwiftUI View in a Mac app. All the solutions I can find (such as this) apply only to iOS since it has UIRectCorner. There is no NSRectCorner equivalent that I can find.
Back in the day, I would round a specific corner of an NSView like this:
layer?.cornerRadius = 5
layer?.maskedCorners = .layerMinXMinYCorner //Bottom-left corner
Has anyone found a way to round specific corners in a Mac app in SwiftUI?

and here comes the answer for macOS :)
// defines OptionSet, which corners to be rounded – same as UIRectCorner
struct RectCorner: OptionSet {
let rawValue: Int
static let topLeft = RectCorner(rawValue: 1 << 0)
static let topRight = RectCorner(rawValue: 1 << 1)
static let bottomRight = RectCorner(rawValue: 1 << 2)
static let bottomLeft = RectCorner(rawValue: 1 << 3)
static let allCorners: RectCorner = [.topLeft, topRight, .bottomLeft, .bottomRight]
}
// draws shape with specified rounded corners applying corner radius
struct RoundedCornersShape: Shape {
var radius: CGFloat = .zero
var corners: RectCorner = .allCorners
func path(in rect: CGRect) -> Path {
var path = Path()
let p1 = CGPoint(x: rect.minX, y: corners.contains(.topLeft) ? rect.minY + radius : rect.minY )
let p2 = CGPoint(x: corners.contains(.topLeft) ? rect.minX + radius : rect.minX, y: rect.minY )
let p3 = CGPoint(x: corners.contains(.topRight) ? rect.maxX - radius : rect.maxX, y: rect.minY )
let p4 = CGPoint(x: rect.maxX, y: corners.contains(.topRight) ? rect.minY + radius : rect.minY )
let p5 = CGPoint(x: rect.maxX, y: corners.contains(.bottomRight) ? rect.maxY - radius : rect.maxY )
let p6 = CGPoint(x: corners.contains(.bottomRight) ? rect.maxX - radius : rect.maxX, y: rect.maxY )
let p7 = CGPoint(x: corners.contains(.bottomLeft) ? rect.minX + radius : rect.minX, y: rect.maxY )
let p8 = CGPoint(x: rect.minX, y: corners.contains(.bottomLeft) ? rect.maxY - radius : rect.maxY )
path.move(to: p1)
path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY),
tangent2End: p2,
radius: radius)
path.addLine(to: p3)
path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY),
tangent2End: p4,
radius: radius)
path.addLine(to: p5)
path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY),
tangent2End: p6,
radius: radius)
path.addLine(to: p7)
path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY),
tangent2End: p8,
radius: radius)
path.closeSubpath()
return path
}
}
// View extension, to be used like modifier:
// SomeView().roundedCorners(radius: 20, corners: [.topLeft, .bottomRight])
extension View {
func roundedCorners(radius: CGFloat, corners: RectCorner) -> some View {
clipShape( RoundedCornersShape(radius: radius, corners: corners) )
}
}

Create a custom shape
import SwiftUI
struct RoundCorner: Shape {
// MARK: - PROPERTIES
var cornerRadius: CGFloat
var maskedCorners: UIRectCorner
// MARK: - PATH
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: maskedCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
return Path(path.cgPath)
}
}
// MARK: - PREVIEW
struct RoundCorner_Previews: PreviewProvider {
static var previews: some View {
RoundCorner(cornerRadius: 20, maskedCorners: .allCorners)
}
}
And Apply this shape to your view
import SwiftUI
struct SwiftUIView: View {
// MARK: - BODY
var body: some View {
Text("Hello, World!")
.padding()
.background(
Color.orange
)
.clipShape(
RoundCorner(
cornerRadius: 12,
maskedCorners: [.topLeft, .bottomRight]
)//OUR CUSTOM SHAPE
)
}
}
// MARK: - PREVIEW
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}

Related

uikit core graphics in pdfkit

I'm trying to develop a graphic for my pdf but, this is the structure:
private func addRowTitle(drawContext: CGContext, offsetY: CGFloat) {
drawContext.saveGState()
drawContext.setLineWidth(2.0)
let attributes = [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15),
NSAttributedString.Key.foregroundColor : UIColor.gray
]
drawContext.move(to: CGPoint(x: 0, y: offsetY + 20))
drawContext.addLine(to: CGPoint(x: pageRect.width, y: offsetY + 20))
drawContext.strokePath()
drawContext.restoreGState()
let rowTitles: [String] = ["Da", "A", "Annulla", "Giorni"]
let tabWidth = pageRect.width / CGFloat(4)
for titleIndex in 0..<rowTitles.count {
print("count \(rowTitles[titleIndex])")
let tabX = CGFloat(titleIndex) * tabWidth
let textRect = CGRect(x: tabX + 40, y: offsetY + 20, // top margin
width: pageRect.width ,height: 15) //pageRect.width - 40 ,height: 40
rowTitles[titleIndex].draw(in: textRect, withAttributes: attributes)
}
drawContext.move(to: CGPoint(x: 0, y: offsetY + 40))
drawContext.addLine(to: CGPoint(x: pageRect.width, y: offsetY + 40))
drawContext.strokePath()
drawContext.restoreGState()
}
and this is the second function involved
private func addRowComunication(drawContext: CGContext, offsetY: CGFloat) {
drawContext.saveGState()
drawContext.setLineWidth(2.0)
let attributes = [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12),
NSAttributedString.Key.foregroundColor : UIColor.black
]
let rowTitles: [String] = ["25/07/2022", "27/07/2022", "10", "30"]
let tabWidth = pageRect.width / CGFloat(4)
for titleIndex in 0..<rowTitles.count {
print("count \(rowTitles[titleIndex])")
let tabX = CGFloat(titleIndex) * tabWidth
let textRect = CGRect(x: tabX + 40, y: offsetY, // top margin
width: pageRect.width ,height: 12) //pageRect.width - 40 ,height: 40
rowTitles[titleIndex].draw(in: textRect, withAttributes: attributes)
}
drawContext.move(to: CGPoint(x: 0, y: offsetY))
drawContext.addLine(to: CGPoint(x: pageRect.width, y: offsetY))
drawContext.strokePath()
drawContext.restoreGState()
}
and finally I call them from
extension PdfCreator {
func pdfData(title : String, body: String) -> Data? {
if let renderer = self.renderer {
let data = renderer.pdfData { context in
context.beginPage()
addTitle(title: title)
//addBody(body: body)
let context = context.cgContext
addWorkerRow(workerName: "Lavoratore 1", workerCF: "RGNNCL)%H!$A$%)V", offsetY: 90)
addRowTitle(drawContext: context, offsetY: 90)
addRowComunication(drawContext: context, offsetY: 100)
}
return data
}
return nil
}
}
the point is I don't understand why if I call both the functions
addRowTitle
addRowComunication
I obtain a table like the attached photo.. with a lot of white space and text is flipped for removing white space in offset I have to put 650 but the text stay flipped. I expect with a offset of 100 to be under the top row
thanks

How to move an object along a path in swiftui

I try to move an Image along a path in a vein. I didn't find how to do it. Here my present code:
struct ContentView: View {
#State var chemin = Path {chemin in
chemin.move(to: CGPoint(x: 620, y: 0))
chemin.addLine(to: CGPoint(x:460, y:120))
chemin.addQuadCurve(to: CGPoint(x: 480, y: 200), control: CGPoint(x: 380, y: 220))
chemin.addLine(to: CGPoint(x:590, y:120))
chemin.addQuadCurve(to: CGPoint(x: 680, y: 320), control: CGPoint(x: 890, y: 160))
chemin.addLine(to: CGPoint(x: 300, y: 330))
chemin.addLine(to: CGPoint(x: 0, y: 160))
}
var body: some View {
ZStack{
Image("carte").resizable().ignoresSafeArea()
VStack{
Image("corona").resizable().frame(width: 150, height: 100, alignment: .center).animation(Animation.linear(duration: 3), value: true)
}
chemin.stroke(Color.green, lineWidth: 3).ignoresSafeArea()
}
}
}
I finally found how to do it, here's the code with some extra-functions:
import SwiftUI
struct FollowEffect: GeometryEffect {
var pct: CGFloat = 0
let path: Path
var rotate = true
var animatableData: CGFloat {
get { return pct }
set { pct = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
if !rotate {
let pt = percentPoint(pct)
return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y))
} else {
// Calculate rotation angle, by calculating an imaginary line between two points
// in the path: the current position (1) and a point very close behind in the path (2).
let pt1 = percentPoint(pct)
let pt2 = percentPoint(pct - 0.01)
let a = pt2.x - pt1.x
let b = pt2.y - pt1.y
let angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pi
let transform = CGAffineTransform(translationX: pt1.x, y: pt1.y).rotated(by: CGFloat(angle))
return ProjectionTransform(transform)
}
}
func percentPoint(_ percent: CGFloat) -> CGPoint {
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
let f = pct > 0.999 ? CGFloat(1-0.001) : pct
let t = pct > 0.999 ? CGFloat(1) : pct + 0.001
let tp = path.trimmedPath(from: f, to: t)
return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY)
}
}
struct ContentView: View {
#State var chemin: Path = Path {path in
path.move(to: CGPoint(x: 620, y: 0))
path.addLine(to: CGPoint(x:460, y:120))
path.addQuadCurve(to: CGPoint(x: 480, y: 200), control: CGPoint(x: 380, y: 220))
path.addLine(to: CGPoint(x:590, y:120))
path.addQuadCurve(to: CGPoint(x: 680, y: 320), control: CGPoint(x: 890, y: 160))
path.addLine(to: CGPoint(x: 300, y: 330))
path.addLine(to: CGPoint(x: 0, y: 160))
}
#State private var flag = false
var body: some View {
GeometryReader { proxy in
ZStack(alignment: .topLeading) {
Image("carte").resizable().ignoresSafeArea()
// Animate movement of Image
Image("corona").resizable().foregroundColor(Color.red).frame(width: 50, height: 50).offset(x: -25, y: -25).modifier(FollowEffect(pct: self.flag ? 1 : 0, path: chemin)).onAppear {
withAnimation(Animation.linear(duration: 4.0).repeatForever(autoreverses: false)) {
self.flag.toggle()
}
}
}.frame(alignment: .topLeading)
}
}
}

SwiftUI + MacOS + Xcode: Format right side of two view HStack such that it is centered in its parent view

I have a view that contains an HStack that contains two views. The right view contains a graph. The left view contains labels for the graphs YAxis. I would like to center the graph in its parent view. If someone could point me in the right direction to accomplish this it would be appreciated. Below is my code.
Regards,
Chris
import SwiftUI
struct MainView: View {
#EnvironmentObject var viewModel : ViewModel
var body: some View {
VStack (alignment: .center) {
let maxYValue = viewModel.data.max { $0.Value < $1.Value }?.Value
let minYValue = viewModel.data.max { $0.Value > $1.Value }?.Value
let deltaY = maxYValue! - minYValue!
let maxYVal = Int(round(maxYValue!))
let minYVal = Int(round(minYValue!))
HStack{
VStack{
Text(String("\(maxYVal)"))
Spacer()
Text("2")
Spacer()
Text("3")
Spacer()
Text("3")
Spacer()
Text(String("\(minYVal)"))
}.frame(width: 100, height: 510, alignment: .trailing)
GeometryReader { geometry in
Path { path in
for index in viewModel.data.indices {
let xPosition = ((geometry.size.width) / (CGFloat(viewModel.data.count - 1))) * CGFloat(index + 0)
let yPosition = (1 - (CGFloat(viewModel.data[index].Value - minYValue!)) / CGFloat(deltaY) ) * (geometry.size.height)
if index == 0 {
path.move(to: CGPoint(x : xPosition, y : yPosition))
}
path.addLine(to: CGPoint(x : xPosition, y: yPosition))
}
} .stroke(Color.blue, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
Path { path in
path.move(to: CGPoint(x: 0, y: geometry.size.height))
path.addLine(to: CGPoint(x: 0, y: 0))
path.move(to: CGPoint(x: 0, y: geometry.size.height))
path.addLine(to: CGPoint(x: geometry.size.width, y: geometry.size.height))
}.stroke(Color.black, style: StrokeStyle(lineWidth: 2))
Path { path in
for index in 0...3 {
path.move(to: CGPoint(x: 0 , y: geometry.size.height * CGFloat(index) / 4))
path.addLine(to: CGPoint(x: geometry.size.width , y: geometry.size.height * CGFloat(index) / 4))
}
}.stroke(Color.gray, style: StrokeStyle(lineWidth: 0.5))
} // end geometry reader
.frame(width: 700, height: 500)// end inner geometry reader
}
} // end of vstack
}
}
Thank you for the response. After posting my query I did some research and came to the conclusion that my code was too complex. So, I rewrote it and I think simplified it. I create a geometry reader and use it to create two rectangles that combined take up the entire parent view. In the upper rectangle I place the graph title and in the lower rectangle I place the graph and am in the process of putting labels on the X and Y axis. Using this approach has allowed me to exactly place items within the rectangle including centering the graph. Problem solved.
The only thing I need to figure out is how to change the stroke for a given line without having to create multiple paths as I have done.
Below is the updated code.
Thanks again for responding.
Chris
import SwiftUI
struct MainView: View {
var body: some View {
GeometryReader { geometry in
VStack (spacing: 0){
let frameWidth = geometry.size.width
let frameHeight = geometry.size.height
Rectangle()
.fill(Color.clear)
// .border(Color.red)
.frame(width: frameWidth, height: frameHeight * 1/8 )
.overlay(
Text("Principal Values Over the Last Year")
.font(.largeTitle)
)
Rectangle()
.fill(Color.clear)
.frame(width: frameWidth, height: frameHeight * 7/8 )
// .border(Color.black, width: 1)
.overlay(
PrincipalChart(frameWidth: frameWidth, frameHeight: frameHeight * 7/8)
)
.padding(0)
} // end Vstack
} // end geometry reader
} // end body var
} // end of MainView
struct PrincipalChart: View {
let frameWidth : CGFloat
let frameHeight : CGFloat
#EnvironmentObject var viewModel : ViewModel
var body: some View {
ZStack {
let maxYValue = viewModel.data.max { $0.Value < $1.Value }?.Value
let minYValue = viewModel.data.max { $0.Value > $1.Value }?.Value
let deltaY = maxYValue! - minYValue!
let maxYVal = Int(round(maxYValue!))
let minYVal = Int(round(minYValue!))
let yAxisStep = Int((maxYVal - minYVal) / 4)
Path { path in
for index in viewModel.data.indices {
let xPosition = (frameWidth * 1/10) + ((frameWidth * 8/10) / (CGFloat(viewModel.data.count - 1))) * CGFloat(index + 0)
let yPosition = (1 - (CGFloat(viewModel.data[index].Value - minYValue!)) / CGFloat(deltaY) ) * (frameHeight * 9/10)
if index == 0 {
path.move(to: CGPoint(x : xPosition, y : yPosition))
}
path.addLine(to: CGPoint(x : xPosition, y: yPosition))
}
} .stroke(Color.blue, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
Path { path in
path.move(to: CGPoint(x: frameWidth * 1/10, y: 0))
path.addLine(to: CGPoint(x: frameWidth * 1/10 , y: frameHeight * 9/10 ))
path.addLine(to: CGPoint(x: frameWidth * 9/10, y: frameHeight * 9/10))
}.stroke(Color.black, style: StrokeStyle(lineWidth: 2))
Path { path in
for index in 0...3 {
path.move(to: CGPoint(x: frameWidth * 1/10, y: (frameHeight * 9/10) * (CGFloat (index) / 4) ))
path.addLine(to: CGPoint(x: frameWidth * 9/10, y: (frameHeight * 9/10) * (CGFloat (index) / 4)))
}
}.stroke(Color.gray, style: StrokeStyle(lineWidth: 0.5))
Text("\(maxYVal)").position(x: (frameWidth * 1/10) - 20, y: 1)
let stepValue3 = maxYVal - yAxisStep
Text("\(stepValue3)").position(x: (frameWidth * 1/10) - 20, y: -2 + (frameHeight * 9/10) * 1 / 4 )
let stepValue2 = stepValue3 - yAxisStep
Text("\(stepValue2)").position(x: (frameWidth * 1/10) - 20, y: -2 + (frameHeight * 9/10) * 2 / 4 )
let stepValue1 = stepValue2 - yAxisStep
Text("\(stepValue1)").position(x: (frameWidth * 1/10) - 20, y: -2 + (frameHeight * 9/10) * 3 / 4 )
Text("\(minYVal)").position(x: (frameWidth * 1/10) - 20, y: -2 + (frameHeight * 9/10) * 4 / 4 )
} // end z stack
} // end body var
} // end Principal Chart view

How to update different views using SwiftUI?

I am aiming to make a program in which I am using using SwiftUI buttons to update by SCNView in SceneKit. I have a cylinder as a SCNCylinder in my SCNView inside a frame in SwiftUI. I want my cylinder to rotate about 180° after I press the button. In my current code I have used #State and #Binding to update the view. But somehow the cylinder rotates as soon as I run the code, not waiting for me to touch the button. Not sure why this happens
Here is my code :
struct ContentView: View {
#State var rotationAngle: Float = 180
var body: some View {
VStack{
Button(action: {
// What to perform
self.rotationAngle = 180
}) {
// How the button looks like
Text("180°")
.frame(width: 100, height: 100)
.position(x: 225, y: 500)
}
SceneKitView(angle: self.$rotationAngle)
.frame(width: 300, height: 300)
.position(x: 225, y: 0)
}
}
}
struct SceneKitView: UIViewRepresentable {
#Binding var angle: Float
func degreesToRadians(_ degrees: Float) -> CGFloat {
return CGFloat(degrees * .pi / 180)
}
func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {
let sceneView = SCNView()
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = UIColor.white
sceneView.frame = CGRect(x: 0, y: 10, width: 0, height: 1)
return sceneView
}
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
let cylinder = SCNCylinder(radius: 0.02, height: 2.0)
let cylindernode = SCNNode(geometry: cylinder)
cylindernode.position = SCNVector3(x: 0, y: 0, z: 0)
cylinder.firstMaterial?.diffuse.contents = UIColor.green
cylindernode.pivot = SCNMatrix4MakeTranslation(0, -1, 0)
let rotation = SCNAction.rotate(by: self.degreesToRadians(self.angle), around: SCNVector3(1, 0, 0), duration: 5)
sceneView.scene?.rootNode.addChildNode(cylindernode)
cylindernode.runAction(rotation)
}
typealias UIViewType = SCNView
}
I want the cylinder to rotate after I press the button. Please help me with this problem.
just set startingAngle to 0
#State var rotationAngle: Float = 0

How to update a SceneKit scene with SwiftUI?

I have a cylinder as SCNCylinder in a SCNScene in SceneKit and want to display it in a frame in SwiftUI. My goal is to rotate the cylinder by a angle of 180° or 90° (as the user chooses). To take the input (of the angle of rotation) i have used Text() and onTapGesture{ .... } property in SwiftUI. After I tap the text, the cylinder rotates but now I have two cylinders, one at the original position and one rotating at an desired angle. I am not sure why this happens. I want the same cylinder to rotate, not a identical copy of that doing it. I have connected the SwiftUI view and SceneKit view by using #State and #Binding.
Here is my code :
struct ContentView: View {
#State var rotationAngle = 0
var body: some View {
VStack{
Text("180°").onTapGesture {
self.rotationAngle = 180
}
Spacer()
Text("90°").onTapGesture {
self.rotationAngle = 90
}
SceneKitView(angle: $rotationAngle)
.position(x: 225.0, y: 200)
.frame(width: 300, height: 300, alignment: .center)
}
}
}
struct SceneKitView: UIViewRepresentable {
#Binding var angle: Int
func degreesToRadians(_ degrees: Float) -> CGFloat {
return CGFloat(degrees * .pi / 180)
}
func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {
let sceneView = SCNView()
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = UIColor.white
sceneView.frame = CGRect(x: 0, y: 10, width: 0, height: 1)
return sceneView
}
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
let cylinder = SCNCylinder(radius: 0.02, height: 2.0)
let cylindernode = SCNNode(geometry: cylinder)
cylindernode.position = SCNVector3(x: 0, y: 0, z: 0)
cylinder.firstMaterial?.diffuse.contents = UIColor.green
cylindernode.pivot = SCNMatrix4MakeTranslation(0, -1, 0)
let inttofloat = Float(self.angle)
let rotation = SCNAction.rotate(by: self.degreesToRadians(inttofloat), around: SCNVector3(1, 0, 0), duration: 5)
cylindernode.runAction(rotation)
sceneView.scene?.rootNode.addChildNode(cylindernode)
}
typealias UIViewType = SCNView
}
I want to have a single cylinder rotation at a given angle.
The problem is, that updateUIView will be called several times. You can check this by adding a debug point there. Because of that your cylinder will be added several times. So you can solve this by many ways...one way would be to delete all nodes in your sceneview before starting your animation like so:
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
sceneView.scene?.rootNode.enumerateChildNodes { (node, stop) in
node.removeFromParentNode()
}

Resources