Draw shape using GeometryReader - swift

I was able to do this:
GeometryReader { geometry in
Capsule()
.foregroundColor(.yellow)
.frame(width: geometry.size.width * 1.7)
.offset(x: geometry.size.width * -0.1 , y: geometry.size.height * -0.9)
}
but I need something like this:
How can I achieve that?
Thanks

There seems to be a maximum width that a view can be before SwiftUI stops letting it get bigger; the capsule/circle shapes seem to hit this which is stopping you from increasing the size of the green shape.
You could try a custom path:
struct ArcShape : Shape {
let geometry: GeometryProxy
func path(in rect: CGRect) -> Path {
var p = Path()
let center = CGPoint(x: 200, y: 100)
p.addArc(center: center, radius: geometry.size.width * 3, startAngle: .degrees(35), endAngle: .degrees(140), clockwise: false)
return p
}
}
struct ExampleView: View {
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Color.white
.edgesIgnoringSafeArea(.all)
ArcShape(geometry: geometry)
.offset(x: geometry.size.width * -0.3, y: geometry.size.height * -1.45)
.foregroundColor(.green)
VStack(alignment: .leading) {
Section{
Text("Bold ").font(.system(size: 18, weight: .bold))
+
Text("light").font(.system(size: 18, weight: .light))
}
Section{
Text("Monday 27 Apr").font(.system(size: 27, weight: .light))
}
Spacer()
}.padding(.horizontal)
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
}
}
}

Related

SwiftUI Pizza Button

I wonder if it's possible to create
so-called "pizza button", which is per se a round view divided in 4 halfs(buttons) diagonally with one more button in center.
Each piece should be clickable
ZStack not working, because only on top button view is clickable.
Please give me a suggestion how can i do this.
My test code: and assets
struct FirstView: View {
var body: some View {
VStack {
HStack {
Button(action: {
}, label: {
Text("Menu")
.frame(width: 88, height: 44)
.background(.gray)
.cornerRadius(12)
})
Spacer()
Button(action: {
}, label: {
Text("Settings")
.frame(width: 88, height: 44)
.background(.gray)
.cornerRadius(12)
})
}
.padding(.horizontal, 40)
ZStack {
Button(action: {
print("1")
}, label: {
Image("left_btn")
.resizable()
.scaledToFit()
.border(.green)
.frame(width: 215, height: 215)
.edgesIgnoringSafeArea(.all)
})
Button(action: {
print("2")
}, label: {
Image("top_btn")
.resizable()
.scaledToFit()
.frame(width: 215, height: 215)
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea()
.border(.yellow)
})
Button(action: {
print("3")
}, label: {
Image("right_btn")
.resizable()
.frame(width: 215, height: 215)
.border(.purple)
})
Button(action: {
print("4")
}, label: {
Image("bottom_btn")
.resizable()
.frame(width: 215, height: 215)
.border(.blue)
})
Button(action: {
print("5")
}, label: {
Image("center_btn")
.resizable()
.frame(width: 87, height: 87)
.border(.orange)
})
}
}
.background(.red)
}
}
[5]
As discussed in the comments, using assets is not the right way to solve this problem.
I used Arcs to draw Pizza Shapes and SFSymbols for the arrow images.
Here's how it looks like:
Pizza Shapes are responsive, but the small circle is not, but you can easily fix this using GeometryReader
the code:
import SwiftUI
struct ContentView: View {
#State private var clicked = false
var body: some View
{
ZStack {
Color.brown.ignoresSafeArea()
ZStack(alignment: .center) {
Group {
PizzaButton(type: .down)
.foregroundColor(clicked ? .teal : .white)
.opacity(clicked ? 0.7 : 1)
.onTapGesture {
print("click")
clicked.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
clicked = false
}
}
PizzaButton(type: .up)
PizzaButton(type: .left)
PizzaButton(type: .right)
}
.foregroundColor(.white)
.animation(.interactiveSpring(), value: clicked)
Circle()
.frame(width: 120, height: 120, alignment: .center) // you can use GeometryReader to make these sizes responsive.
.foregroundColor(.init(white: 0.98))
Circle()
.strokeBorder(lineWidth: 4)
.frame(width: 30, height: 30, alignment: .center)
.foregroundColor(.init(white: 0.28))
}
.frame(width: 400, height: 400, alignment: .center)
}
}
}
struct PizzaButton : View {
let type : ButtonType
let alignment : Alignment
let edge : Edge.Set
let arrowDirection : String
init (type : ButtonType) {
self.type = type
switch (type) {
case .up:
alignment = .top
edge = .top
arrowDirection = "up"
case .right:
alignment = .trailing
edge = .trailing
arrowDirection = "forward"
case .left:
alignment = .leading
edge = .leading
arrowDirection = "backward"
case .down:
alignment = .bottom
edge = .bottom
arrowDirection = "down"
}
}
var body: some View {
GeometryReader { geo in
ZStack(alignment: alignment) {
PizzaButtonShape(type: type)
.stroke(lineWidth: 1)
.background(PizzaButtonShape(type: type)) // this is required because .stroke clears the shapes background.
Image(systemName: "arrow.\(arrowDirection)")
.foregroundColor(.init(white: 0.35))
.font(.largeTitle)
.padding(edge, geo.size.width / 5)
}
}
}
}
struct PizzaButtonShape : Shape {
let type : ButtonType
func path(in rect: CGRect) -> Path {
var path = Path()
let startAngle : Angle
let endAngle : Angle
switch(type) {
case .up:
startAngle = .degrees(225)
endAngle = .degrees(315)
break
case .down:
startAngle = .degrees(45)
endAngle = .degrees(135)
break
case .right:
startAngle = .degrees(315)
endAngle = .degrees(45)
break
case .left:
startAngle = .degrees(135)
endAngle = .degrees(225)
break
}
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.height / 3, startAngle: startAngle, endAngle: endAngle, clockwise: false)
return path
}
}
enum ButtonType {
case left, right, up, down
}

SwiftUI Custom Shape doesn't work correctly

I have a custom separator as a Shape. I also have a view on which I put my separator (I add it 3 times and separated by spacers).
Below I added a capsule with spacing 4. According to the idea my custom separator and capsule should have the same spacings.
Why doesn’t it work like that?
// Separator struct
struct ProgressBarSeparator: View{
struct RightShape: Shape {
func path(in rect: CGRect) -> Path {
let offsetX = rect.maxX - rect.width / 4
let crect = CGRect(origin: .zero, size: CGSize(width: 4, height: 4)).offsetBy(dx: offsetX, dy: .zero)
var path = Rectangle().path(in: rect)
path.addPath(Circle().path(in: crect))
return path
}
}
struct LeftShape: Shape {
func path(in rect: CGRect) -> Path {
let offsetX = rect.minX - rect.width / 4
let crect = CGRect(origin: .zero, size: CGSize(width: 4, height: 4)).offsetBy(dx: offsetX, dy: .zero)
var path = Rectangle().path(in: rect)
path.addPath(Circle().path(in: crect))
return path
}
}
var body: some View {
Rectangle()
.frame(width: 8, height: 4)
.foregroundColor(.gray)
.mask(RightShape().fill(style: .init(eoFill: true)))
.mask(LeftShape().fill(style: .init(eoFill: true)))
}
}
// Progress bar view
private var progressBar: some View {
GeometryReader { proxy in
ZStack(alignment: .leading) {
Capsule()
.foregroundColor(ColorName.black400.color)
.frame(height: 4)
Capsule()
.fill(gradient)
.frame(width: (proxy.size.width - 12) * 0 / 4, height: 4)
.animation(.easeInOut)
HStack.zeroSpacing {
Spacer()
ProgressBarSeparator()
Spacer()
ProgressBarSeparator()
Spacer()
ProgressBarSeparator()
Spacer()
}
}
}
}
// Example capsule
private var procentCashback: some View {
HStack(spacing: 4) {
ForEach(0 ... 3) { index in
ZStack(alignment: .leading) {
Capsule()
.fill(.orange)
.frame(height: 4)
}
}
}
}
My broken view

SwiftUI: Change the start position for animated progress bar (or Shape drawing in general)

The code below begins to draw a rectangle from the bottom left corner. How can I change it to begin to draw from the top middle? Hopefully there is a simple setting I am missing here.
#State private var progress: Double = 0
#State private var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
HStack{
ZStack {
Rectangle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.gray)
.rotationEffect(Angle(degrees: 270.0))
.frame(width: 100, height:100)
Rectangle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.blue)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
.frame(width: 100, height:100)
.onReceive(timer, perform: { _ in
if(progress < 100){
progress += 0.1/10
}
else{
progress = 0
}
})
}
}
.padding()
}
}
You don't need to rotate it (moreover it might be not square), just create own rectangular shape with start point anywhere needed.
Demo prepared with Xcode 12.4 / iOS 14.4
struct MyRectangle: Shape {
func path(in rect: CGRect) -> Path {
Path {
$0.move(to: CGPoint(x: rect.midX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
$0.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
$0.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
}
}
}
struct ContentView: View {
#State private var progress: Double = 0
#State private var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
HStack{
ZStack {
MyRectangle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.gray)
.frame(width: 200, height:100)
MyRectangle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.blue)
.animation(.linear)
.frame(width: 200, height:100)
.onReceive(timer, perform: { _ in
if(progress < 100){
progress += 0.1/10
}
else{
progress = 0
}
})
}
}
.padding()
}
}
}

Multidevice layout SwiftUI

I have this layout working fine in iPhone 11, but when I switch to 8, the top background color gets disproportionate, how can I make it have the same height? Is there such a thing as height %?
and the one that gets weird:
This is the code:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
HomeView()
.tabItem {
VStack {
Image(systemName: "1.circle")
Text("Home")
}
}.tag(1)
}
}
}
struct HomeView: View {
var body: some View {
ZStack {
SetBackground()
Text("Home View")
.font(.largeTitle)
}
}
}
struct ArcShape : Shape {
let geometry: GeometryProxy
func path(in rect: CGRect) -> Path {
var p = Path()
let center = CGPoint(x: 290, y: 100)
p.addArc(center: center, radius: geometry.size.width * 3, startAngle: .degrees(39), endAngle: .degrees(140), clockwise: false)
return p
}
}
struct SetBackground: View {
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Color.white
.edgesIgnoringSafeArea(.all)
ArcShape(geometry: geometry)
.offset(x: geometry.size.width * -0.3, y: geometry.size.height * -1.49)
.foregroundColor(.yellow)
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
}
}
}
The rest is working fine, it's just that top background that shifts if the iPhone model changes. I tried putting it in a frame but that just gets it weird on the middle :/
Also, why does it changes if I put the tabView and only leave HomeView()? It's so weird
Thanks
It is due to hardcoded Arc center & different safe-areas on different devices.
So the solution is to make Arc position geometry dependent (specific factors you can fit)
struct ArcShape : Shape {
let geometry: GeometryProxy
func path(in rect: CGRect) -> Path {
var p = Path()
let center = CGPoint(x: geometry.size.width / 2, y: -geometry.size.height * 1.75) // 1/4 from top
p.addArc(center: center, radius: geometry.size.height * 2, startAngle: .degrees(39), endAngle: .degrees(140), clockwise: false)
return p
}
}
struct SetBackground: View {
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Color.white
ArcShape(geometry: geometry)
.foregroundColor(.yellow)
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
}
}
}

SwiftUI Custom Slider with max Value

I use this great code to get a slider. here
but how can i set the max value to 30 not to 100?
this example is from 0 to 100.
hope everyone can help.
struct CustomView: View {
#Binding var percentage: Float // or some value binded
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.accentColor)
.frame(width: geometry.size.width * CGFloat(self.percentage / 100))
}
.cornerRadius(12)
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ value in
self.percentage = min(max(0, Float(value.location.x / geometry.size.width * 100)), 100)
}))
}
}
}
You just need to replace the 100 with 30 to get bound from 0 to 30
struct CustomView: View {
#Binding var percentage: Float // or some value binded
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.accentColor)
.frame(width: geometry.size.width * CGFloat(self.percentage / 30))
}
.cornerRadius(12)
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ value in
self.percentage = min(max(0, Float(value.location.x / geometry.size.width * 30)), 30)
print(self.percentage)
}))
}
}
}