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
}
Related
I'm trying to make a loading view with some animations, but instead of the RoundRectangle offset it self horizontaly as it's defined in the code, it is actually moving in diagonal.
Why is it animating in diagonal?
Here's my struct:
struct LoadingIndicator: View {
let textToDisplay:String
#State private var isLoading:Bool = false
var body: some View {
ZStack {
Text(textToDisplay)
.fontWeight(.bold)
.offset(x: 0, y: -25)
RoundedRectangle(cornerRadius: 3)
.stroke(Color.gray, lineWidth: 3)
.frame(width: 250, height: 3)
RoundedRectangle(cornerRadius: 3)
.stroke(Color.indigo, lineWidth: 3)
.frame(width: 30, height: 3)
.offset(x: (isLoading ? 110 : -110), y: 0)
.animation(.linear(duration: 1).repeatForever(), value: isLoading)
}.onAppear {
self.isLoading = true
}
}
}
This is what I got:
This is what I wanted to achieve:
After seing this question: SwiftUI: Broken explicit animations in NavigationView?
I solved my problem:
struct LoadingIndicator: View {
let textToDisplay:String
#State private var isLoading:Bool = false
var body: some View {
ZStack {
Text(textToDisplay)
.fontWeight(.bold)
.offset(x: 0, y: -25)
RoundedRectangle(cornerRadius: 3)
.stroke(Color.gray, lineWidth: 3)
.frame(width: 250, height: 3)
RoundedRectangle(cornerRadius: 3)
.stroke(Color.indigo, lineWidth: 3)
.frame(width: 30, height: 3)
.offset(x: (isLoading ? 110 : -110), y: 0)
.animation(.linear(duration: 1).repeatForever(), value: isLoading)
}.onAppear {
DispatchQueue.main.async {
self.isLoading = true
}
}
}
}
I have a horizontally scrolling ScrollView in SwiftUI. I need to somehow center the element that is shown more than a half. I can't figure out how to get ScrollView position in percent, and I don't know how to optimize using GeometryReader for all possible devices.
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach (sectionData) { item in
GeometryReader { geometry in
SectionView(section: item)
.rotation3DEffect(
Angle(degrees: Double(geometry.frame(in:.global).minX - 30) / -20),
axis: (x: 0.0, y: 10.0, z: 0.0))
}
.frame(width: 275, height: 275)
}
}
.padding(30)
.padding(.bottom, 30)
}
.offset(y: -30)
This could be an aproach
public struct SectionView: View {
var section: Int
public var body: some View {
ZStack {
Rectangle()
.frame(width: 275, height: 275, alignment: .center)
Text("\(section)")
.foregroundColor(.white)
}
}
}
public struct ExampleView: View {
#State private var isShowingNumber: Int = 0
public var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { value in
ZStack (alignment: .center) {
LazyHStack(alignment: .center, spacing:275) {
ForEach (1..<20) { item in
Rectangle()
.frame(width: 1, height: 1, alignment: .center)
.foregroundColor(.clear)
.onAppear() {
print(item)
withAnimation() {
value.scrollTo(item, anchor: .center)
}
}
}
}
HStack(spacing:10) {
ForEach (1..<20) { item in
GeometryReader { geometry in
ZStack {
SectionView(section: item)
.id(item)
.rotation3DEffect(
Angle(degrees: Double(geometry.frame(in:.global).minX - 30) / -20),
axis: (x: 0.0, y: 10.0, z: 0.0))
}
}
.frame(width: 275, height: 275)
}
}
.padding(30)
.padding(.bottom, 30)
}
}
}
.offset(y: -30)
}
}
So I am building a blackjack simulator game and my problem right now is making it more realistic. From when I press the "Hit Me" button I'd like a delay before my next card is uploaded. Similarily when the next player goes I'd also like a delay. So right now I have aplayer view a hand view and a card view. The player and hand are both observables.
var body: some View {
HStack {
VStack(alignment: .trailing, spacing: 0){
ForEach(0..<self.player.hands.count, id: \.self) {
index in ZStack {
Spacer()
HandView.init(hand: self.player.hands[index])
}
}
Spacer().frame(height: 45)
Text(self.player.id).bold().font(Font.system(size: 20))
Spacer()
}
.padding(.all)
if !player.isRobot{
VStack{Button(action: {
self.player.requestCard()
}, label: {
Text("Hit Me!")
})
Button(action: {
self.player.handleInput(turn: turnPosibilities.stay)
self.player.objectWillChange.send()
}, label: {
Text("Stay")
})}
.offset(x: 10, y: 0)}
}
}
#ObservedObject var hand:Hand
var bust: some View {
GeometryReader { geometry in
Path { path in
path.move(to: CGPoint.init(x: geometry.frame(in: .local).midX, y: CGFloat(geometry.frame(in: .local).midY)))
path.addLine(to: CGPoint.init(x: geometry.frame(in: .local).minX, y: geometry.frame(in: .local).minY))
path.addLine(to: CGPoint.init(x: geometry.frame(in: .local).minX, y: geometry.frame(in: .local).maxY))
path.addLine(to: CGPoint.init(x: geometry.frame(in: .local).maxX, y: geometry.frame(in: .local).maxY))
path.addLine(to: CGPoint.init(x: geometry.frame(in: .local).maxX, y: geometry.frame(in: .local).minY))
}
.fill(Color.red)
}
}
var body: some View {
ZStack
{ForEach(0..<self.hand.cards.count, id: \.self){
card in
VStack(alignment: .leading, spacing: 2) {PlayingCardView(rank: self.hand.cards[card].rankRaw, suit: self.hand.cards[card].suit, isFaceUp: self.hand.cards[card].isFaceUp ).rotationEffect(Angle.init(degrees: Double(multiply(index: card, offset: 10))))
.offset(x: multiply(index: card, offset: 15), y: multiply(index: card, offset: 40))
}
}
}
}
}
ZStack{
if isFaceUp {
Rectangle().frame(width: 130, height:182).foregroundColor(Color.init(#colorLiteral(red: 0.9999127984, green: 1, blue: 0.9998814464, alpha: 1))).cornerRadius(25).overlay(Image(self.suit.rawValue).resizable().aspectRatio(contentMode: .fit).padding(.all))
Text(String(self.rank)).font(.custom("Poppins-SemiBoldItalic", size: 40)).offset(x: 29, y: -70)
Text(String(self.rank)).font(.custom("Poppins-SemiBoldItalic", size: 40)).offset(x: 29, y: -70).rotationEffect(Angle.init(degrees: 180))
}
else {
Rectangle().frame(width: 130, height:182).foregroundColor(Color.init(UIColor(red: 0.58, green: 0.65, blue: 0.65, alpha: 1.00)
)).cornerRadius(25)
}
}.clipShape(RoundedRectangle(cornerRadius: 25)).overlay(RoundedRectangle(cornerRadius: 25).stroke(Color.black, lineWidth: 2))
}
Here is possible approach
VStack{Button(action: {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // 1 sec delay
self.player.requestCard()
}
}, label: {
Text("Hit Me!")
})
This can be achieved in SwiftUI by defining a #State variable / flag and showing new view based on that. In the below code snippet showSecondView determines when to show the next view.
struct ContentView: View {
#State var showSecondView = false
var body: some View {
Group {
Text("Hello, World!")
.font(.largeTitle)
.foregroundColor(.primary)
if showSecondView {
Text("SecondView")
.font(.title)
.foregroundColor(.secondary)
}
}
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (_) in
withAnimation {
self.showSecondView = true
}
}
}
}
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)
}
}
}
The task
I am creating a bar chart with SwiftUI and I am looking for the best approach to position the average indicator, the horizontal yellow line, shown in the picture below:
The goal
I need to be able to position the yellow line (rectangle with 1 point in height) relative to the bars (also rectangles) of the chart. For simplicity let us imagine that the tallest possible bar is 100 points and the average is 75. My goal is to draw the yellow line exactly 75 points above the bottom of the bars.
The problem
In my example I am placing the line on top of the chart using a ZStack. I can position the line vertically using a GeometryReader, however I do not know how to position it in reference to the bottom of the bars.
Here my playground code:
import SwiftUI
import PlaygroundSupport
struct BarChart: View {
var measurements : [Int]
var pastColor: Color
var todayColor: Color
var futureColor: Color
let labelsColor = Color(UIColor(white: 0.5, alpha: 1.0))
func monthAbbreviationFromInt(_ day: Int) -> String {
let da = Calendar.current.shortWeekdaySymbols
return da[day]
}
var body: some View {
ZStack {
VStack {
HStack {
Rectangle()
.fill(Color.yellow)
.frame(width: 12, height: 2, alignment: .center)
Text("Desired level")
.font(.custom("Helvetica", size: 10.0))
.foregroundColor(labelsColor)
}
.padding(.bottom , 50.0)
HStack {
ForEach(0..<7) { day in
VStack {
Spacer()
Text(String(self.measurements[day]))
.frame(width: CGFloat(40), height: CGFloat(20))
.font(.footnote)
.lineLimit(1)
.minimumScaleFactor(0.5)
.foregroundColor(self.labelsColor)
Rectangle()
.fill(day > 0 ? self.futureColor : self.pastColor)
.frame(width: 20, height: CGFloat(self.measurements[day]*2))
.transition(.slide)
.cornerRadius(3.0)
Text("\(self.monthAbbreviationFromInt(day))\n\(day)")
.multilineTextAlignment(.center)
.font(.footnote)
.frame(height: 20)
.lineLimit(2)
.minimumScaleFactor(0.2)
.foregroundColor(self.labelsColor)
}
.frame(height: 200, alignment: .bottom )
}
}
}
.padding()
VStack {
GeometryReader { geometry in
//Yellow line
Rectangle()
.path(in: CGRect(x: 0,
y: 350, //This is now a random value, but I should detect and use the bottom coordinate of the bars to place the line at exactly 75 points above that coordinate
width: geometry.size.width,
height: 1))
.fill(Color.yellow)
}
}
}
}
}
How can I get the relative position of the bottom of the bars so that I can place the line correctly?
Is there another approach you would suggest?
Thank you in advance
example
import SwiftUI
struct Data: Identifiable {
let id = UUID()
let value: CGFloat
let color: Color
}
struct ContentView: View {
let data = [Data(value: 35, color: .red),
Data(value: 100, color: .green),
Data(value: 70, color: .yellow),
Data(value: 25, color: .blue),
Data(value: 55, color: .orange)]
var maxValue: CGFloat {
data.map { (element) -> CGFloat in
element.value
}.max() ?? 1.0
}
var average: CGFloat {
guard !data.isEmpty else { return 0 }
let sum = data.reduce(into: CGFloat.zero) { (res, data) in
res += data.value
}
return sum / CGFloat(data.count)
}
var body: some View {
HStack {
ForEach(data) { (bar) in
Bar(t: bar.value / self.maxValue, color: bar.color, width: 20)
}
}
.background(Color.pink.opacity(0.1))
.overlay(
GeometryReader { proxy in
Color.primary.frame(height: 1).position(x: proxy.size.width / 2, y: proxy.size.height * ( 1 - self.average / self.maxValue))
}
).padding(.vertical, 100).padding(.horizontal, 20)
}
}
struct Bar: View {
let t: CGFloat
let color: Color
let width: CGFloat
var body: some View {
GeometryReader { proxy in
Path { (path) in
path.move(to: .init(x: proxy.size.width / 2, y: proxy.size.height))
path.addLine(to: .init(x: proxy.size.width / 2, y: 0))
}
.trim(from: 0, to: self.t)
.stroke(lineWidth: self.width)
.foregroundColor(self.color)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and result
or slightly adopted to show labels