Add drop shadow to SwiftUI Path elements - ios

I have a rudimentary ClockView build with SwiftUI. :) My question is if applying drop shadows to the clock-hands is easily possible with this approach or if i need a different layout and grouping of the elements? I've tried to find a way to add shadows to Path, but got stuck. Thank you.
Code
import SwiftUI
struct ClockView: View {
#Binding var time : TimeValue
let hoursHandWidth = 10
let minutesHandWidth = 10
let secondsHandWidth = 10
var body: some View {
return(
ZStack {
// clock-face
Path { path in
let seconds : Path = Path(CGRect(x: 100, y: -0.5, width: 10, height: 1))
for i in 0...60 {
…
}
}
.fill(Color(red: 0.3, green: 0.3, blue: 0.3, opacity: 1.0))
Path { path in
let hours : Path = Path(CGRect(x: 100, y: -1, width: 12, height: 2))
for i in 0...12 {
…
}
}
.fill(Color(red: 0.3, green: 0.3, blue: 0.3, opacity: 1.0))
Path { path in
let threehours : Path = Path(CGRect(x: 95, y: -2, width: 20, height: 4))
for i in 0...4 {
…
}
}
.fill(Color.red)
// clock-hands
Path { path in
let minutehand : Path = Path(CGRect(x: 0, y: -(minutesHandWidth/2), width: 95, height: minutesHandWidth))
path.addPath(minutehand, …)
}
.fill(Color.blue)
Path { path in
let hourhand : Path = Path(CGRect(x: 0, y: -(hoursHandWidth/2), width: 72, height: hoursHandWidth))
path.addPath(hourhand, …)
}
.fill(Color.blue)
Path { path in
let secondshand : Path = Path(CGRect(x: 0, y: -(secondsHandWidth/2), width: 97, height: secondsHandWidth))
path.addPath(secondshand, …)
}
.fill(Color.yellow)
.animation(.easeInOut)
Path { path in
path.addPath(Path(CGPath(ellipseIn: CGRect(x: -5, y: -5, width: 10, height: 10), transform: nil)))
}
.fill(Color.black)
}
.offset(CGSize(width: 150, height: 150))
.frame(width: 300, height: 300, alignment: .topLeading)
.scaleEffect(1.2)
.background(Color(red: 0.8, green: 0.8, blue: 0.8, opacity: 1.0))
)
}
}
Look
** Update **
I've applied .shadow(..) as user Asperi recommended:
Path { path in
let secondshand : Path = Path(CGRect(x: 0, y: -(secondsHandWidth/2), width: 97, height: secondsHandWidth))
path.addPath(secondshand, transform: .init(rotationAngle: 2.0 * CGFloat.pi * CGFloat(time.seconds-15)/60.0))
}
.fill(Color.yellow)
.shadow(color: .black, radius: 8, x: 1, y: 1)
But the result is not exactly what i expected :)

I've moved the transformation (rotation) from the subpath to the resulting path. Now it looks good.
Old:
Path { path in
let secondshand : Path = Path(CGRect(x: 0, y: -(secondsHandWidth/2), width: 97, height: secondsHandWidth))
path.addPath(secondshand, transform: .init(rotationAngle: 2.0 * CGFloat.pi * CGFloat(time.seconds-15)/60.0))
}
.fill(Color.yellow)
.shadow(color: .black, radius: 2)
New:
Path { path in
let secondshand : Path = Path(CGRect(x: 0, y: -(secondsHandWidth/2), width: 97, height: secondsHandWidth))
path.addPath(secondshand)
}
.fill(Color.yellow)
.shadow(color: .black, radius: 2)
.transformEffect(CGAffineTransform(rotationAngle: 2.0 * CGFloat.pi * CGFloat(time.seconds-15)/60.0))
Look (new):

Here is an example
Path { path in
let secondshand : Path = Path(CGRect(x: 0, y: -(secondsHandWidth/2), width: 97, height: secondsHandWidth))
path.addPath(secondshand)
}
.fill(Color.yellow)
.shadow(color: .black, radius: 8, x: 4, y: 1)

Related

BezierPath clipShape issue

I added curves on the right and left sides. When you look at the stroke function, everything is normal, but when the stroke function is removed, the curve on the right works normally, but the curve on the left does not work. is this a bug?
ContentView
struct ContentView: View {
#State var viewState: CGSize = .zero
var body: some View {
ZStack {
Color.orange
.clipShape(FooBezierPath(rightOffset: viewState).stroke())//remove stroke
.edgesIgnoringSafeArea(.all)
.overlay(
HStack {
Image(systemName: "chevron.right")
.font(.largeTitle)
.offset(y: 115)
.edgesIgnoringSafeArea(.all)
Spacer()
Image(systemName: "chevron.left")
.font(.largeTitle)
.contentShape(Rectangle())
.gesture(DragGesture().onChanged({ (value) in
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
self.viewState = value.translation
}
}).onEnded({ (value) in
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
self.viewState = .zero
}
}))
.edgesIgnoringSafeArea(.all)
.offset(y: 70)
}
,alignment: .topTrailing
)
.padding(.horizontal)
}
}
}
BezierPath
struct FooBezierPath: Shape {
var rightOffset: CGSize
func path(in rect: CGRect) -> Path {
return Path { path in
let width = rect.width + rightOffset.width
let height = rect.height
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: 0))
//MARK: - Left Curve
path.move(to: CGPoint(x: 0, y: 80))
path.addCurve(to: CGPoint(x: 0, y: 180), control1: CGPoint(x: 50, y: 130), control2: CGPoint(x: 50, y: 130))
//MARK: - Right Curve
path.move(to: CGPoint(x: width, y: 80))
path.addCurve(to: CGPoint(x: width, y: 180), control1: CGPoint(x: width - 50, y: 130), control2: CGPoint(x: width - 50, y: 130))
}
}
}
It Helps to Draw the entire shape without moving points, otherwise the clip malfunctions for some reason. Try this:
struct FooBezierPath: Shape {
var rightOffset: CGSize
func path(in rect: CGRect) -> Path {
return Path { path in
let width = rect.width + rightOffset.width
let height = rect.height
// Draw left Edge of Shape
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: 80))
path.addCurve(to: CGPoint(x: 0, y: 180), control1: CGPoint(x: 50, y: 130), control2: CGPoint(x: 50, y: 130))
path.addLine(to: CGPoint(x: 0, y: height))
// Draw Bottom Edge of Shape
path.addLine(to: CGPoint(x: width, y: height))
// Draw Right Edge of Shape
path.addLine(to: CGPoint(x: width, y: 180))
path.addCurve(to: CGPoint(x: width, y: 80), control1: CGPoint(x: width - 50, y: 130), control2: CGPoint(x: width - 50, y: 130))
path.addLine(to: CGPoint(x: width, y: 0))
// Draw Top Edge of Shape
path.addLine(to: CGPoint(x:0, y: 0))
}
}
}
Tried and tested the code, the results are:
Without Stroke
With Stroke

How to combine shapes in SwiftUI?

I need to create a shape from several other shapes, and ideally, I'd get a single shape struct at the end which would have two shapes stacked as ZStack would. I haven't noticed an obvious implementation of this anywhere, so maybe somebody has some ideas on how to implement it?
Here's what I want to have:
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
// Add shapes here???? For example, combine Rectangle with Circle?
}
}
You can use func addPath(_ path: Path, transform: CGAffineTransform = .identity) to create a new Shape like this :
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
var customShape = Path { p in
p.move(to: CGPoint(x: 0, y: 0))
p.addQuadCurve(to: CGPoint(x: 0, y: 100),
control: CGPoint(x: 0, y: 0))
p.addCurve(to: CGPoint(x: 100, y: 400),
control1: CGPoint(x: 0, y: 200),
control2: CGPoint(x: 100, y: 200))
p.addCurve(to: CGPoint(x: 200, y: 100),
control1: CGPoint(x: 100, y: 200),
control2: CGPoint(x: 200, y: 200))
p.addQuadCurve(to: CGPoint(x: 200, y: 0),
control: CGPoint(x: 200, y: 0))
}
let rectangle = Rectangle()
.path(in: customShape.boundingRect)
.offsetBy(dx: 100, dy: 0)
customShape.addPath(rectangle, transform: .init(scaleX: 0.5, y: 0.35))
return customShape
}
}
And use it like this :
struct SwiftUIView: View {
var body: some View {
CustomShape()
}
}

How to erase the thin side line right bottom?

I want compound view's shape from Oval shape subtract Rect shape,
so first I create a mask image:
let paths = CGMutablePath()
//Oval shape path at left
let leftPath = CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 200, height: 150), transform: nil)
//Rect shape path at right
let rightPath = CGPath(roundedRect: CGRect(x: 100, y: 100, width: 120, height: 100), cornerWidth: 8, cornerHeight: 8, transform: nil)
paths.addPath(leftPath)
paths.addPath(rightPath)
UIGraphicsBeginImageContext(CGSize(width: 220, height: 200))
let ctx = UIGraphicsGetCurrentContext()
ctx?.addPath(paths)
ctx?.clip(using: .evenOdd)
ctx?.addPath(leftPath.cgPath)
ctx?.clip(using: .evenOdd)
ctx?.setFillColor(UIColor.red.cgColor)
ctx?.fill(CGRect(x: 0, y: 0, width: 220, height: 200))
//the mask image
let maskImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
next I use this image for mask:
let maskView = UIImageView(image: maskImage)
maskView.contentMode = .center
imageView.mask = maskView
run App, I get this:
looks good?not really...
if you look carefully,you will notice a thin line at view right bottom
It's not OK for me!
How do I erase the sideline??? Thanks :)
Draw the ellipse, then clear the rounded rect.
import UIKit
let ellipse = CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 200, height: 150), transform: nil)
let roundedRect = CGPath(roundedRect: CGRect(x: 100, y: 100, width: 120, height: 100), cornerWidth: 8, cornerHeight: 8, transform: nil)
let maskImage = UIGraphicsImageRenderer(size: CGSize(width: 220, height: 200)).image { rendererContext in
let ctx = rendererContext.cgContext
ctx.setFillColor(UIColor.red.cgColor)
ctx.addPath(ellipse)
ctx.fillPath()
ctx.setBlendMode(.clear)
ctx.addPath(roundedRect)
ctx.fillPath()
}
Result:

How do a gradient path that setting the colors from path begin to end

I want to draw a gradient path that from a color in the begin and end in another color.
Here are two ways to draw gradient line but the effect neither I want.
Someone has any idea to do this?
guard let c = UIGraphicsGetCurrentContext() else {
fatalError("Current Context NOT Founds.")
}
c.setLineWidth(width)
c.setLineCap(cap)
c.setLineJoin(join)
// the way no.1
c.saveGState()
c.move(to: CGPoint(x: 50, y: 50))
c.addLine(to: CGPoint(x: 50, y: 200))
c.addLine(to: CGPoint(x: 150, y: 200))
let alphas: [CGFloat] = [0, 1]
let colors = [UIColor(white: 1, alpha: alphas[0]).cgColor, UIColor(white: 1, alpha: alphas[1]).cgColor] as CFArray
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors, locations: [0.0, 1.0])
c.addRect(.infinite)
c.replacePathWithStrokedPath()
c.clip()
c.drawLinearGradient(gradient!, start: CGPoint(x: 0, y: 0), end: CGPoint(x: rect.width, y: rect.height), options: CGGradientDrawingOptions(rawValue: 0))
c.restoreGState()
// the way no.2
c.saveGState()
c.beginTransparencyLayer(in: rect, auxiliaryInfo: nil)
c.move(to: CGPoint(x: 50, y: 250))
c.addLine(to: CGPoint(x: 50, y: 450))
c.addLine(to: CGPoint(x: 130, y: 350))
c.setStrokeColor(UIColor.white.cgColor)
c.strokePath()
c.setBlendMode(.sourceIn)
c.drawLinearGradient(gradient!, start: CGPoint(x: 50, y: 220), end: CGPoint(x: 50, y: 450), options: CGGradientDrawingOptions(rawValue: 0))
c.endTransparencyLayer()
c.restoreGState()
last update 2017/05/26
Here are some sample images that the effect is similar to I want.

UIView confusion

All , I am trying to figure out a UIView which gets bigger with this code :
ColouredSquare.backgroundColor = UIColor(red: 0.84, green: 0.90, blue: 0.95, alpha: 1.0)
ColouredSquare.frame = CGRect(x: 0, y:50, width: 400, height: 30)
self.view.addSubview(ColouredSquare)
UIView.animateWithDuration(1.0, animations: {
self.ColouredSquare.frame = CGRect(x: 0, y: 50, width: 400, height: 180)
})
I have added a button within the view.
But the button draws before the UIView.
Am I missing something obvious here?
You set the frame:
ColouredSquare.frame = CGRect(x: 0, y:50, width: 400, height: 30)
And then animate is, increasing the height to 180:
self.ColouredSquare.frame = CGRect(x: 0, y: 50, width: 400, height: 180)

Resources