I am trying to create a shaded cross-hatched. But so far I can do it by adding a Image.
How can I create a custom view in which lines will be drawn and not filled with a Image?
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Image("lineFilledBG").resizable().clipShape(Circle())
Circle().stroke()
Circle().foregroundColor(.yellow).opacity(0.3)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is how it looks now. Want lines to draw on top of another view or shape, without adding opacity and image pattern filling.
Thank to Cenk Bilgen for stripe pattern. Tweaked a bit so that you can rotate the hatch for any shape.
import SwiftUI
import CoreImage.CIFilterBuiltins
extension CGImage {
static func generateStripePattern(
colors: (UIColor, UIColor) = (.clear, .black),
width: CGFloat = 6,
ratio: CGFloat = 1) -> CGImage? {
let context = CIContext()
let stripes = CIFilter.stripesGenerator()
stripes.color0 = CIColor(color: colors.0)
stripes.color1 = CIColor(color: colors.1)
stripes.width = Float(width)
stripes.center = CGPoint(x: 1-width*ratio, y: 0)
let size = CGSize(width: width, height: 1)
guard
let stripesImage = stripes.outputImage,
let image = context.createCGImage(stripesImage, from: CGRect(origin: .zero, size: size))
else { return nil }
return image
}
}
extension Shape {
func stripes(angle: Double = 45) -> AnyView {
guard
let stripePattern = CGImage.generateStripePattern()
else { return AnyView(self)}
return AnyView(Rectangle().fill(ImagePaint(
image: Image(decorative: stripePattern, scale: 1.0)))
.scaleEffect(2)
.rotationEffect(.degrees(angle))
.clipShape(self))
}
}
And usage
struct ContentView: View {
var body: some View {
VStack {
Rectangle()
.stripes(angle: 30)
Circle().stripes()
Capsule().stripes(angle: 90)
}
}
}
Output image link
I'm not sure this is what you want, but CoreImage can generate a striped pattern.
import CoreImage.CIFilterBuiltins
import SwiftUI
extension CGImage {
// width is for total, ratio is second stripe relative to full width
static func stripes(colors: (UIColor, UIColor), width: CGFloat, ratio: CGFloat) -> CGImage {
let filter = CIFilter.stripesGenerator()
filter.color0 = CIColor(color: colors.0)
filter.color1 = CIColor(color: colors.1)
filter.width = Float(width-width*ratio)
filter.center = CGPoint(x: width, y: 0)
let size = CGSize(width: width+width*ratio, height: 1)
let bounds = CGRect(origin: .zero, size: size)
// keep a reference to a CIContext if calling this often
return CIContext().createCGImage(filter.outputImage!.clamped(to: bounds), from: bounds)!
}
}
then use ImagePaint
Circle().fill(
ImagePaint(image: Image(decorative: CGImage.stripes(colors: (.blue, .yellow), width: 80, ratio: 0.25), scale: 1))
)
.rotationEffect(.degrees(-30))
Or CILineScreen filter may be more what you want.
I`m not sure what do you want but another possible solution is using different backgrounds in a ZStack linear gradient and shadows to make convex or concave effect., for this it helps that your background with a little color, not white white. some samples:
The code:
struct ContentView: View {
var body: some View {
ZStack {
Color.yellow.opacity(0.1)
VStack(spacing: 50) {
myCircle()
myCircle1()
}
}
}
}
struct myCircle: View {
var body: some View {
Circle().foregroundColor(Color.clear)
.frame(width: 100, height: 100)
.background(
ZStack {
LinearGradient(gradient: Gradient(colors: [Color( #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) ), Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1))]), startPoint: .topLeading, endPoint: .bottomTrailing)
Circle()
.stroke(Color.clear, lineWidth: 10)
.shadow(color: Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)), radius: 3, x: -5, y: -5)
Circle()
.stroke(Color.clear, lineWidth: 10)
.shadow(color: Color(#colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1)), radius: 3, x: 3, y: 3)
}
)
.clipShape(Circle())
.shadow(color: Color( #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) ), radius: 20, x: -20, y: -20)
.shadow(color: Color( #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) ), radius: 20, x: 20, y: 20)
}
}
struct myCircle1: View {
var body: some View {
Circle().foregroundColor(Color.clear)
.frame(width: 100, height: 100)
.background(
ZStack {
LinearGradient(gradient: Gradient(colors: [Color( #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) ), Color(#colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1))]), startPoint: .topLeading, endPoint: .bottomTrailing)
Circle()
.stroke(Color.clear, lineWidth: 10)
.shadow(color: Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)), radius: 3, x: -5, y: -5)
Circle()
.stroke(Color.clear, lineWidth: 10)
.shadow(color: Color(#colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1)), radius: 3, x: 3, y: 3)
}
)
.clipShape(Circle())
.shadow(color: Color( #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) ), radius: 20, x: -20, y: -20)
.shadow(color: Color( #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) ), radius: 20, x: 20, y: 20)
}
}
Thank you to Cenk Bilgen and flyer2001, I edited the code and come up with this code so it can be used with a view.
import SwiftUI
import CoreImage.CIFilterBuiltins
extension CGImage {
static func generateStripePattern(
colors: (UIColor, UIColor) = (.clear, .black),
width: CGFloat = 6,
ratio: CGFloat = 1) -> CGImage? {
let context = CIContext()
let stripes = CIFilter.stripesGenerator()
stripes.color0 = CIColor(color: colors.0)
stripes.color1 = CIColor(color: colors.1)
stripes.width = Float(width)
stripes.center = CGPoint(x: 1 - (width * ratio), y: 0)
let size = CGSize(width: width, height: 1)
guard
let stripesImage = stripes.outputImage,
let image = context.createCGImage(stripesImage, from: CGRect(origin: .zero, size: size))
else { return nil }
return image
}
}
struct ViewStripes : ViewModifier {
var stripeColor : Color
var width : CGFloat
var ratio : CGFloat
var angle : Double
var frameW : CGFloat
var frameH : CGFloat
func body(content: Content) -> some View {
if let stripePattern = CGImage.generateStripePattern(colors: (.clear,UIColor(stripeColor)), width: width, ratio: ratio) {
content
.overlay(Rectangle().fill(ImagePaint(image: Image(decorative: stripePattern, scale: 1.0)))
.frame(width: hypotenuse((frameW / 2), frameH / 2) * 2, height: hypotenuse(frameW / 2, frameH / 2) * 2)
//.scaleEffect(1)
.rotationEffect(.degrees(angle))
.mask(content))
}
}
}
extension View {
func drawStripes(stripeColor: Color, width: CGFloat, ratio: CGFloat, angle : Double, frameW : CGFloat, frameH : CGFloat) -> some View {
modifier(ViewStripes(stripeColor: stripeColor, width: width, ratio: ratio, angle: angle, frameW: frameW, frameH: frameH))
}
}
func hypotenuse(_ a: Double, _ b: Double) -> Double {
return (a * a + b * b).squareRoot()
}
And for usage:
Rectangle()
.foregroundColor(Color.Red)
.frame(width:20, height: 20)
//This is the code itself
.drawStripes(stripeColor: Color.white, width: 3, ratio: 0.9, angle: 45, frameW: 20, frameH: 20)
Hope it may be helpful for someone.
Related
I have an issue with the shadow shape not being animated at the same time as the discs.
Here's the corresponding code. You can test this on an iPad by changing the orientation.
This is a direct consequence of the ContentView. Removing the GeometryReader and using a fixed frame fixes it.
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
PlanetView()
.frame(width: geometry.size.width * 0.25, height: geometry.size.width * 0.25)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct PlanetView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
Circle()
.fill(.blue)
Circle()
.foregroundColor(.green)
.overlay(PlanetShadowView())
.frame(width: geometry.size.width * 0.8, height: geometry.size.width * 0.8)
}
}
}
}
struct PlanetShadowView: View {
var offsetRatio: CGFloat = 15/100.0
func control1(_ size: CGSize) -> CGPoint {
let xOffset = offsetRatio * size.width
let yOffset = offsetRatio * size.height
return .init(x: xOffset, y: size.height - yOffset)
}
func control2(_ size: CGSize) -> CGPoint {
let xOffset = offsetRatio * size.width
let yOffset = offsetRatio * size.height
return .init(x: size.width - xOffset, y: size.height - yOffset)
}
var body: some View {
GeometryReader { geometry in
Path { path in
path.move(to: CGPoint(x: 0, y: geometry.size.height / 2))
path.addCurve(to: CGPoint(x: geometry.size.width, y: geometry.size.height / 2),
control1: control1(geometry.size),
control2: control2(geometry.size))
path.addArc(center: CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2),
radius: geometry.size.width / 2,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 180),
clockwise: false)
path.closeSubpath()
}
.fill(Color.black.opacity(0.25))
.rotationEffect(.degrees(-30))
}
}
}
struct PlanetView_Previews: PreviewProvider {
static var previews: some View {
PlanetView()
}
}
A solution for this is to make planet shadow as a shape, so it would render in the same GeometryReader transformation context as everything else.
Tested with Xcode 13.4 / iOS 15.5
Circle()
.foregroundColor(.green)
.overlay(
PlanetShadow() // << here !!
.fill(Color.black.opacity(0.25))
.rotationEffect(.degrees(-30)))
.frame(width: geometry.size.width * 0.8, height: geometry.size.width * 0.8)
}
}
}
}
struct PlanetShadow: Shape { // << here !!
var offsetRatio: CGFloat = 15/100.0
func path(in rect: CGRect) -> Path {
// calc here ...
Test module on GitHub
Add .drawingGroup(). This should render the composition as a whole before displaying it.
I edited my question. I shared full code.
I want to bend the end points of the semicircle as in the 1st image. How can I do that ? I tried this code but got an unrelated error.
How can I soften the corners of a half circle?
I created a customShape but I couldn't get the look I wanted.
.clipShape(
circle()
.trim(from: 0.5, to: 1)
.stroke(Color.red, style: StrokeStyle(lineWidth: 30, lineCap: .round, lineJoin: .round))
Error:
ChartData Container:
class ChartDataContainer : ObservableObject {
#Published var chartData =
[ChartData(color: Color(#colorLiteral(red: 1, green: 0.4932718873, blue: 0.4739984274, alpha: 1)), percent: 8, value: 0),
ChartData(color: Color(#colorLiteral(red: 1, green: 0.8323456645, blue: 0.4732058644, alpha: 1)), percent: 15, value: 0),
ChartData(color: Color(#colorLiteral(red: 0.4508578777, green: 0.9882974029, blue: 0.8376303315, alpha: 1)), percent: 32, value: 0),
ChartData(color: Color(#colorLiteral(red: 0.476841867, green: 0.5048075914, blue: 1, alpha: 1)), percent: 45, value: 0)]
func calc(){
var value : CGFloat = 0
for i in 0..<chartData.count {
value += chartData[i].percent
chartData[i].value = value
}
}
}
Custom Shape:
struct CustomShape: View {
var body: some View {
HalfCircle()
.stroke(.red, style: StrokeStyle(lineWidth: 30, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
}
}
struct CustomShape_Previews: PreviewProvider {
static var previews: some View {
CustomShape()
}
}
struct HalfCircle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.height/2, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 180), clockwise: true)
return path
}
}
Code:
struct HalfDonutChart : View {
#ObservedObject var charDataObj = ChartDataContainer()
#State var indexOfTappedSlice = -1
var body: some View {
ZStack {
ZStack {
ForEach(0..<charDataObj.chartData.count) { index in
Circle()
.trim(from: 0.5, to: 1)
.trim(from: index == 0 ? 0.0 : charDataObj.chartData[index-1].value/100, to: charDataObj.chartData[index].value/100)
.stroke(charDataObj.chartData[index].color,lineWidth: 30)
.onTapGesture {
indexOfTappedSlice = indexOfTappedSlice == index ? -1 : index
}
.scaleEffect(index == indexOfTappedSlice ? 1.1 : 1.0)
.animation(.spring())
}
if indexOfTappedSlice != -1 {
Text(String(format: "%.2f", Double(charDataObj.chartData[indexOfTappedSlice].percent))+"%")
.font(.title)
}
}
.mask(
CustomShape()
)
.frame(width: 200, height: 250)
.padding()
.onAppear() {
self.charDataObj.calc()
}
}
}
}
i have this card thing in the picture to build, here're the code:
struct ArticleCard: View {
var article: Article
var body: some View {
ZStack {
Color.white
.border(Color(red: 0, green: 0, blue: 0, opacity: 0.2))
.shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.2), radius: 0.0, x: 5, y: 5)
VStack{
Text(article.title)
.padding(.top, 5)
.padding(.leading)
.padding(.trailing)
.font(.title2)
Image(article.coverUrl).resizable()
.frame(height: 200)
.padding(.leading)
.padding(.trailing)
Text(article.title)
.padding(.leading)
.padding(.trailing)
.padding(.bottom)
}
}
}
}
in the ZStack i put a Color.white there as a background of the card, and give this color view a shadow, but the color seems to be transparent, therefor i got unwanted lines on the top and the left inside the borders, how do i get rid of them?
You are using border and shadow in wrong place, try this:
PS: there is no reason of using Color(red: 0, green: 0, blue: 0, opacity: 0.2) you can use this: Color.black.opacity(0.2)
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Color.white
VStack {
Text("article.title")
.padding()
Image(systemName: "star")
.resizable()
.scaledToFit()
Text("article.title")
.padding()
}
}
.frame(width: 300, height: 300, alignment: .center)
.border(Color(red: 0, green: 0, blue: 0, opacity: 0.2))
.compositingGroup()
.shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.2), radius: 0.0, x: 5, y: 5)
}
}
I refactored your code for better and less code as possible you can use Image instead of Text in your App.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("your custom Text")
.padding()
Text("🥩")
.font(Font.system(size: 150))
Text("your custom Text")
.padding()
}
.frame(width: 300, height: 300, alignment: .center)
.background(Color.white)
.border(Color.black.opacity(0.2))
.compositingGroup()
.shadow(color: Color.black.opacity(0.2), radius: 0.0, x: 5, y: 5)
}
}
Based on the value of a #State var, how can I animate any other parameters but one? For instance, I want to animate .offset but not the .foregroundColor. It may sound like an easy task to do, but it's not so easy when using the same "base view" to create some sort of cell views and combining different modifiers and transition.
For the following animation, I'm trying to "avoid" that change-color animation while allowing the offset to happen - I need the change of color to happen instantaneously, no animation.
For this animation, I'm also trying to "avoid" that change-color animation. The challenge here is that the base view also has a transition.
The "base view" name is LogoView. Here's my code:
struct Test_ForegroundColorAnimation: View {
var array: [Color] = [.yellow, .green, .blue]
#State var activeIndex1: Int = -1
#State var activeIndex2: Int = -1
#State var show: Bool = true
private func animationFor(index: Int) -> Animation {
Animation
.easeInOut(duration: 0.85)
.delay(0.15 * Double(array.count - index))
}
func zindexfor(index: Int) -> Double {
Double(index == activeIndex1 ? 100 : index)
}
func opacityfor(index: Int) -> Double {
self.activeIndex1 >= 0 ? (index == self.activeIndex1 ? 1 : 0) : 1
}
var body: some View {
VStack(alignment: .leading, spacing: 25) {
Spacer()
VStack {
Text("Offset animation").font(.title)
Text("Click any item").font(.subheadline)
}
ZStack {
ForEach(array.indices, id: \.self) { index in
LogoView(isSelected: index == self.activeIndex1, color: self.array[index])
.offset(x: self.activeIndex1 == -1 ? CGFloat(90 * index) : 0)
.zIndex(self.zindexfor(index: index))
.opacity(self.opacityfor(index: index))
.animation(self.animationFor(index: index))
.onTapGesture {
self.activeIndex1 = index == self.activeIndex1 ? -1 : index
}
}
}
.padding(.bottom, 70)
VStack {
Text("Transition").font(.title)
Text("Click any item").font(.subheadline)
}
Button(action: {
self.show.toggle()
}) {
Color.purple
.frame(width: 100, height: 70)
.overlay(Text("Activate transition!").foregroundColor(.white))
}
HStack(spacing: 5) {
ForEach(array.indices, id: \.self) { index in
HStack {
if self.show {
LogoView(isSelected: index == self.activeIndex2, color: self.array[index])
.transition(AnyTransition.move(edge: .bottom))
.onTapGesture {
self.activeIndex2 = index == self.activeIndex2 ? -1 : index
}
}
}
.frame(width: 90, height: 90)
.animation(self.animationFor(index: index))
}
}
}
}
}
struct Test_ForegroundColorAnimation_Previews: PreviewProvider {
static var previews: some View {
Test_ForegroundColorAnimation()
}
}
struct LogoView: View {
var isSelected: Bool = false
var color: Color = Color(#colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1))
var shadow: Color = Color(#colorLiteral(red: 0.4392156899, green: 0.01176470611, blue: 0.1921568662, alpha: 1))
var body: some View {
Image(systemName: "heart.fill")
.resizable()
.frame(width: 28, height: 28)
.foregroundColor(isSelected ? Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)) : color)
.padding()
.overlay(
Circle()
.stroke(lineWidth: 3)
.foregroundColor(color)
)
.background(
Circle()
.foregroundColor(isSelected ? color : Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)))
.shadow(color: isSelected ? color.opacity(0.7) : Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)).opacity(0.3), radius: 5, x: 0, y: 10)
)
.frame(width: 90, height: 90)
}
}
If I correctly understood what you need, here is a fix
struct LogoView: View {
var isSelected: Bool = false
var color: Color = Color(#colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1))
var shadow: Color = Color(#colorLiteral(red: 0.4392156899, green: 0.01176470611, blue: 0.1921568662, alpha: 1))
var body: some View {
Image(systemName: "heart.fill")
.resizable()
.frame(width: 28, height: 28)
.foregroundColor(isSelected ? Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)) : color)
.padding()
.overlay(
Circle()
.stroke(lineWidth: 3)
.foregroundColor(color)
)
.background(
Circle()
.foregroundColor(isSelected ? color : Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)))
.animation(nil) // << fix !!
.shadow(color: isSelected ? color.opacity(0.7) : Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)).opacity(0.3), radius: 5, x: 0, y: 10)
)
.frame(width: 90, height: 90)
}
}
I am trying to do something like this, but I do not know where to begin.
I have some code so far which "works" sort of..
here is my full code (macOS)
struct ContentView: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
var body: some View {
ZStack {
Path { path in
path.move(to: CGPoint(x: 20, y: 100))
path.addLine(to: CGPoint(x: currentPosition.width, y: currentPosition.height))
}
.trimmedPath(from: 0, to: 1)
.strokedPath(StrokeStyle(lineWidth: 9, lineCap: .square, lineJoin: .round))
.foregroundColor(.red)
Circle()
.frame(width: 7, height: 7)
.foregroundColor(Color.blue)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
.gesture(DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
print(self.newPosition.width)
self.newPosition = self.currentPosition
}
)
Color.clear
}
}
}
But my blue circle is far away from the rectangle.
Any help please?
You are simply missing the position of the Circle.
struct ContentView: View {
#State private var startPoint: CGPoint = .zero
#State private var endPoint: CGPoint = CGPoint(x: 100, y: 100)
var body: some View {
ZStack {
Path { (path) in
path.move(to: startPoint)
path.addLine(to: endPoint)
}
.strokedPath(StrokeStyle(lineWidth: 9, lineCap: .square, lineJoin: .round))
.foregroundColor(.red)
//Circle 1
Circle()
.frame(width: 16, height: 16)
.position(startPoint)
.foregroundColor(.blue)
.gesture(DragGesture()
.onChanged { (value) in
self.startPoint = CGPoint(x: value.location.x, y: value.location.y)
})
//Circle 2
Circle()
.frame(width: 16, height: 16)
.position(endPoint)
.foregroundColor(.green)
.gesture(DragGesture()
.onChanged { (value) in
self.endPoint = CGPoint(x: value.location.x, y: value.location.y)
})
Color.clear
}
}
}