Working with dial view in SwiftUI with drag gesture and displaying value of selected number - swift

I'm new to SwiftUI and what I trying to build is a dial view like this.
As you can see it has:
a view indicator which is for displaying the currently selected value from the user.
CircularDialView with all the numbers to chose from.
text view for displaying actual selected number
Maximum what I tried to accomplish is that for now CircularDialView is intractable with a two-finger drag gesture and for the rest, I have no even close idea how to do it.
Please help, because I tried everything I could and have to say it's super hard to do with SwiftUI
Here's the code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Rectangle()
.frame(width: 4, height: 25)
.padding()
CircularDialView()
Text("\(1)")
.font(.title)
.fontWeight(.bold)
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ClockText: View {
var numbers: [String]
var angle: CGFloat
private struct IdentifiableNumbers: Identifiable {
var id: Int
var number: String
}
private var dataSource: [IdentifiableNumbers] {
numbers.enumerated().map { IdentifiableNumbers(id: $0, number: $1) }
}
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(dataSource) {
Text("\($0.number)")
.position(position(for: $0.id, in: geometry.frame(in: .local)))
}
}
}
}
private func position(for index: Int, in rect: CGRect) -> CGPoint {
let rect = rect.insetBy(dx: angle, dy: angle)
let angle = ((2 * .pi) / CGFloat(numbers.count) * CGFloat(index)) - .pi / 2
let radius = min(rect.width, rect.height) / 2
return CGPoint(x: rect.midX + radius * cos(angle),
y: rect.midY + radius * sin(angle))
}
}
struct CircularDialView: View {
#State private var rotateState: Double = 0
var body: some View {
ZStack {
Circle()
.stroke(Color.black, lineWidth: 5)
.frame(width: 250, height: 250, alignment: .center)
ClockText(
numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map {"\($0)"},
angle: 45
)
.font(.system(size: 20))
.frame(width: 290, height: 290, alignment: .center)
}
.rotationEffect(Angle(degrees: rotateState))
.gesture(
RotationGesture()
.simultaneously(with: DragGesture())
.onChanged { value in
self.rotateState = value.first?.degrees ?? rotateState
}
)
}
}

Related

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 Create a Animating Circle

I'd like to create some content like this, a blinking green circle, and it works in the single preview mode
But when I put the View inside a List, the Green circle start moving left and right
struct DotView: View {
#State var delay: Double = 0 // 1.
#State var scale: CGFloat = 0.5
var body: some View {
Circle()
.frame(width: 6, height: 6)
.foregroundColor(Color.green)
.scaleEffect(scale)
.animation(Animation.easeInOut(duration: 0.6).repeatForever().delay(delay)) // 2.
.onAppear {
withAnimation {
self.scale = 1
}
}
}
}
Using inside a navigation view
List {
VStack {
HStack {
Text(server.name)
.fontWeight(.bold)
.foregroundColor(Color.primary)
.fontWeight(.light)
.foregroundColor(.gray)
Spacer()
DotView()
}
}
}
As I wrote in comments it is not reproducible for me, but try the following...
struct DotView: View {
var delay: Double = 0 // 1. << don't use state for injectable property
#State private var scale: CGFloat = 0.5
var body: some View {
Circle()
.frame(width: 6, height: 6)
.foregroundColor(Color.green)
.scaleEffect(scale)
.animation(
Animation.easeInOut(duration: 0.6)
.repeatForever().delay(delay), value: scale // 2. << link to value
)
.onAppear {
self.scale = 1 // 3. << withAnimation no needed now
}
}
}

How to move an object in Ellipse path in SwiftUI?

Ellipse Path
i am trying to move an object in ellipse path.
but i don't know correct way and i think it need some of math and equation that i don't know until now
image of output
import SwiftUI
struct RotateObjectInEllipsePath: View {
let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
#State private var angle: Double = .zero
var body: some View {
VStack {
// circle shape
Circle()
.strokeBorder(Color.red, lineWidth: 2)
.frame(width: 250, height: 250)
.overlay(
Image(systemName: "arrow.down")
.offset(x: 250/2)
.rotationEffect(.degrees(angle))
)
//Spacer
Spacer()
.frame(height: 100)
// ellipse shape
Ellipse()
.strokeBorder(Color.red, lineWidth: 2)
.frame(width: 250, height: 150)
.overlay(
Image(systemName: "arrow.down")
.offset(x: 250/2)
.rotationEffect(.degrees(angle))
)
}// VStack
.animation(.default)
.onReceive(timer) { _ in
angle += 1
}
}
struct RotateObjectInEllipsePath_Previews: PreviewProvider {
static var previews: some View {
RotateObjectInEllipsePath()
}}
i found this code in community
x = x_center + Acos(2pi*t/T);
y = y_center + Bsin(2pi*t/T);
When A == B, the trajectory is a circumference
Only change the order between .rotationEffect and .offset, and the offset can be synchronized the ellipse trajectory and also can be synchronized with circumference trajectory.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct RotateObjectInEllipsePath: View {
let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
let a: CGFloat = 250.0
let b: CGFloat = 150.0
#State private var angle: Double = .zero
#State private var ellipseX: CGFloat = .zero
#State private var ellipseY: CGFloat = .zero
var body: some View {
VStack {
Circle()
.strokeBorder(Color.red, lineWidth: 2)
.frame(width: a, height: a)
.overlay(
Image(systemName: "arrow.down")
.offset(x: a/2)
.rotationEffect(.degrees(angle))
)
Spacer()
.frame(height: 60)
Ellipse()
.strokeBorder(Color.red, lineWidth: 2)
.frame(width: a, height: b)
.overlay(
Image(systemName: "arrow.down")
.rotationEffect(.degrees(angle))
.offset(x: ellipseX, y: ellipseY)
)
Spacer()
.frame(height: 40)
}// VStack
.animation(.default)
.onReceive(timer) { _ in
angle += 1
let theta = CGFloat(.pi * angle / 180)
ellipseX = a / 2 * cos(theta)
ellipseY = b / 2 * sin(theta)
}
}
}
struct RotateObjectInEllipsePath_Previews: PreviewProvider {
static var previews: some View {
RotateObjectInEllipsePath()
}
}

How to correctly do up an adjustable split view in SwiftUI?

This is my first time trying out SwiftUI, and I am trying to create a SwiftUI view that acts as a split view, with an adjustable handle in the center of the two views.
Here's my current code implementation example:
struct ContentView: View {
#State private var gestureTranslation = CGSize.zero
#State private var prevTranslation = CGSize.zero
var body: some View {
VStack {
Rectangle()
.fill(Color.red)
.frame(height: (UIScreen.main.bounds.height / 2) + self.gestureTranslation.height)
RoundedRectangle(cornerRadius: 5)
.frame(width: 40, height: 3)
.foregroundColor(Color.gray)
.padding(2)
.gesture(DragGesture()
.onChanged({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
})
.onEnded({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
self.prevTranslation = self.gestureTranslation
})
)
Rectangle()
.fill(Color.green)
.frame(height: (UIScreen.main.bounds.height / 2) - self.gestureTranslation.height)
}
}
}
How it looks like now:
[
This kinda works, but when dragging the handle, it is very glitchy, and that it seems to require a lot of dragging to reach a certain point.
Please advice me what went wrong. Thank you.
See How to change the height of the object by using DragGesture in SwiftUI? for a simpler solution.
My version of that:
let MIN_HEIGHT = CGFloat(50)
struct DragViewSizeView: View {
#State var height: CGFloat = MIN_HEIGHT
var body: some View {
VStack {
Rectangle()
.fill(Color.red)
.frame(width: .infinity, height: height)
HStack {
Spacer()
Rectangle()
.fill(Color.gray)
.frame(width: 100, height: 10)
.cornerRadius(10)
.gesture(
DragGesture()
.onChanged { value in
height = max(MIN_HEIGHT, height + value.translation.height)
}
)
Spacer()
}
VStack {
Text("my o my")
Spacer()
Text("hoo hah")
}
}
}
}
struct DragTestView: View {
var body: some View {
VStack {
DragViewSizeView()
Spacer() // If comment this line the result will be as on the bottom GIF example
}
}
}
struct DragTestView_Previews: PreviewProvider {
static var previews: some View {
DragTestView()
}
}
From what I have observed, the issue seems to be coming from the handle being repositioned while being dragged along. To counteract that I have set an inverse offset on the handle, so it stays in place. I have tried to cover up the persistent handle position as best as I can, by hiding it beneath the other views (zIndex).
I hope somebody else got a better solution to this question. For now, this is all that I have got:
import PlaygroundSupport
import SwiftUI
struct SplitView<PrimaryView: View, SecondaryView: View>: View {
// MARK: Props
#GestureState private var offset: CGFloat = 0
#State private var storedOffset: CGFloat = 0
let primaryView: PrimaryView
let secondaryView: SecondaryView
// MARK: Initilization
init(
#ViewBuilder top: #escaping () -> PrimaryView,
#ViewBuilder bottom: #escaping () -> SecondaryView)
{
self.primaryView = top()
self.secondaryView = bottom()
}
// MARK: Body
var body: some View {
GeometryReader { proxy in
VStack(spacing: 0) {
self.primaryView
.frame(height: (proxy.size.height / 2) + self.totalOffset)
.zIndex(1)
self.handle
.gesture(
DragGesture()
.updating(self.$offset, body: { value, state, _ in
state = value.translation.height
})
.onEnded { value in
self.storedOffset += value.translation.height
}
)
.offset(y: -self.offset)
.zIndex(0)
self.secondaryView.zIndex(1)
}
}
}
// MARK: Computed Props
var handle: some View {
RoundedRectangle(cornerRadius: 5)
.frame(width: 40, height: 3)
.foregroundColor(Color.gray)
.padding(2)
}
var totalOffset: CGFloat {
storedOffset + offset
}
}
// MARK: - Playground
let splitView = SplitView(top: {
Rectangle().foregroundColor(.red)
}, bottom: {
Rectangle().foregroundColor(.green)
})
PlaygroundPage.current.setLiveView(splitView)
Just paste the code inside XCode Playground / Swift Playgrounds
If you found a way to improve my code please let me know.

SwiftUI - Position a rectangle relative to another view in ZStack

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