SwiftUI Custom Shape doesn't work correctly - swift

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

Related

How to implement a slider whose minimum track color is begin from center(value=0) not left in SwiftUI

I want to custom Slider in SwiftUI. Just something like this.
I tried Slider with GeometryReader but it isn't working.
//MARK: Left - Right Balance
GeometryReader { geo in
VStack {
Text("\(String(format: "%.2f", balanceVolume))")
HStack {
Text("L")
Slider(value: $balanceVolume, in: minValue...maxValue, step: 0.1) {editing in
print("editing", editing)
isEditing = editing
if !editing {
player.pan = Float(balanceVolume)
}
}
.tint(.none)
.accentColor(.gray)
Text("R")
}
}
.padding(20)
}
Thank you you all.
I have created a simple custom slider, I hope it helps
Output:
Use:
struct slider: View {
#State var sliderPosition: Float = 50
var body: some View {
SliderView(value: $sliderPosition, bounds: 1...100).padding(.all)
}
}
Code:
struct SliderView: View {
let currentValue: Binding<Float>
let sliderBounds: ClosedRange<Int>
public init(value: Binding<Float>, bounds: ClosedRange<Int>) {
self.currentValue = value
self.sliderBounds = bounds
}
var body: some View {
GeometryReader { geomentry in
sliderView(sliderSize: geomentry.size)
}
}
#ViewBuilder private func sliderView(sliderSize: CGSize) -> some View {
let sliderViewYCenter = sliderSize.height / 2
let sliderViewXCenter = sliderSize.width / 2
ZStack {
RoundedRectangle(cornerRadius: 2)
.fill(Color.gray)
.frame(height: 3)
ZStack {
let sliderBoundDifference = sliderBounds.count
let stepWidthInPixel = CGFloat(sliderSize.width) / CGFloat(sliderBoundDifference)
let thumbLocation = CGFloat(currentValue.wrappedValue) * stepWidthInPixel
// Path between starting point to thumb
lineBetweenThumbs(from: .init(x: sliderViewXCenter, y: sliderViewYCenter), to: .init(x: thumbLocation, y: sliderViewYCenter))
// Thumb Handle
let thumbPoint = CGPoint(x: thumbLocation, y: sliderViewYCenter)
thumbView(position: thumbPoint, value: Float(currentValue.wrappedValue))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let dragLocation = dragValue.location
let xThumbOffset = min(dragLocation.x, sliderSize.width)
let newValue = Float(sliderBounds.lowerBound / sliderBounds.upperBound) + Float(xThumbOffset / stepWidthInPixel)
if newValue > Float(sliderBounds.lowerBound) && newValue < Float(sliderBounds.upperBound + 1) {
currentValue.wrappedValue = newValue
}
})
}
}
}
#ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View {
Path { path in
path.move(to: from)
path.addLine(to: to)
}.stroke(Color.blue, lineWidth: 4)
}
#ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View {
ZStack {
Text(String(round(value)))
.font(.headline)
.offset(y: -20)
Circle()
.frame(width: 24, height: 24)
.foregroundColor(.accentColor)
.shadow(color: Color.black.opacity(0.16), radius: 8, x: 0, y: 2)
.contentShape(Rectangle())
}
.position(x: position.x, y: position.y)
}
}

Custom Slider Taking Too Much Space

I used a code from here to create my own variation of a custom slider. However, the slider has too much extra space at the bottom, and I would like to remove that. Is there any way to do so?
Here is the code of the slider:
struct CustomSlider: View {
#Binding var value: CGFloat
#State var lastOffset: CGFloat = 0
var range: ClosedRange<CGFloat>
var leadingOffset: CGFloat = 5
var trailingOffset: CGFloat = 5
var knobSize: CGSize = CGSize(width: 25, height: 25)
let trackGradient = LinearGradient(gradient: Gradient(colors: [.pink, .yellow]), startPoint: .leading, endPoint: .trailing)
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
ZStack(alignment: .leading) {
Capsule()
.fill(Color(hue: 0.172, saturation: 0.275, brightness: 1.0))
.frame(height: 30)
Capsule()
.fill(Color(hue: 0.55, saturation: 0.326, brightness: 1.0))
.frame(width: self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset))+30, height: 30)
}
HStack {
RoundedRectangle(cornerRadius: 50)
.frame(width: self.knobSize.width, height: self.knobSize.height)
.foregroundColor(.white)
.offset(x: self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset)))
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
if abs(value.translation.width) < 0.1 {
self.lastOffset = self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset))
}
let sliderPos = max(0 + self.leadingOffset, min(self.lastOffset + value.translation.width, geometry.size.width - self.knobSize.width - self.trailingOffset))
let sliderVal = sliderPos.map(from: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset), to: self.range)
self.value = sliderVal
}
)
}
}
}
}
}
I have noticed that when I select the empty space, the code cased in geometry reader is automatically selected. (I couldn't embed the image in this post, so here's the link)
When you use GeometryReader, it automatically takes up all available space. If you'd like to constrain it, use a frame(height: ) modifier to give it a certain height.
struct CustomSlider: View {
//all of the properties
var body: some View {
GeometryReader { geometry in
// slider code
}.frame(height: 60) //<-- Here
}
}

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

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
}
)
}
}

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)
}
}
}

Draw shape using GeometryReader

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)
}
}
}