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
}
}
}
Related
This is the view I am trying to create:
Everything except the orange line is in place and working (the green ones are just helper lines for visualisation).
I am trying to retrieve informations on rendered views and set view properties to their rect to calculate the orange Path. Right now, this results in the Modifying state during view update, this will cause undefined behavior. error.
What do I need to change in order to make this work?
Here's my view:
struct MyView: View {
#State private var titleRect: CGRect = .zero
#State private var dividerRect: CGRect = .zero
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25.0)
.foregroundColor(.yellow)
VStack {
Text("Some fancy title …")
.border(.green) //
.background {
GeometryReader { proxy -> Color in
titleRect = proxy.frame(in: .named("root"))
return .clear
}
}
Rectangle()
.frame(height: 20.0)
.foregroundColor(.clear)
Rectangle()
.frame(height: 1.0)
.background {
GeometryReader { proxy -> Color in
dividerRect = proxy.frame(in: .named("root"))
return .clear
}
}
Spacer()
}
.padding(.top)
Path { path in
path.move(to: CGPoint(x: dividerRect.minX, y: titleRect.maxY)) // startingPoint
path.addLine(to: CGPoint(x: dividerRect.width * 2/3, y: dividerRect.maxY)) // midPoint
path.addLine(to: CGPoint(x: dividerRect.maxX, y: titleRect.maxY)) // endPoint
}
}
.padding()
.coordinateSpace(name: "root")
}
}
I think I found a good workaround for my problem by implementing a ViewModifier that can get me the frame size for arbitrary views I need it for:
struct MeasureSizeModifier: ViewModifier {
let callback: (CGSize) -> ()
func body(content: Content) -> some View {
content
.background {
GeometryReader { proxy in
Color.clear
.onAppear {
callback(proxy.size)
}
}
}
}
}
extension View {
func measureSize(_ callback: #escaping (CGSize) -> () ) -> some View {
modifier(MeasureSizeModifier(callback: callback))
}
}
Credits go out to "Flo Writes Code" on YouTube for his tutorial on this: How to use GeometryReader
BEST in SwiftUl!
After having implemented what is suggested, I can use this extension like so in my specific case:
struct MyView: View {
#State private var containerWidth: CGFloat = .zero
#State private var titleHeight: CGFloat = .zero
var body: some View {
ZStack(alignment: .top) {
RoundedRectangle(cornerRadius: 25.0)
.measureSize { size in
containerWidth = size.width
print(containerWidth)
}
.foregroundColor(.yellow)
Text("Some fancy title …")
.border(.green)
.measureSize { size in
titleHeight = size.height
}
Path { path in
path.move(to: CGPoint(x: .zero, y: titleHeight))
path.addLine(to: CGPoint(x: containerWidth * 0.67, y: titleHeight * 3))
path.addLine(to: CGPoint(x: containerWidth, y: titleHeight))
path.move(to: CGPoint(x: .zero, y: titleHeight * 3))
path.addLine(to: CGPoint(x: containerWidth, y: titleHeight * 3))
}
.stroke(.black)
.border(.green)
HStack {
Text("Y")
Spacer()
Text("N")
}
.padding(.horizontal, 8)
.position(x: containerWidth / 2, y: titleHeight * 2)
.frame(width: containerWidth)
}
.padding()
}
}
I will have other places where I will be glad to have this extension in my toolbelt 🎉
The rectangle should always be centered in ContainerView no matter what scale offset or anchor point innerContainerView has.
What offset is needed to place the rectangle in the center of the ContainerView?
let innerContainerSize = CGSize(width: 100, height: 100)
struct innerContainerView: View {
#Binding var ia: [si]
var body: some View {
ZStack() {
ForEach(ia) { i in
Rectangle()
.fill(.green)
.scaleEffect(i.scale)
.offset(x: i.frame.origin.x, y: i.frame.origin.y)
.frame(width: 500 * 0.7, height: 500 * 0.7)
}
}
}
}
struct ContainerView: View {
#Binding var ia: [si]
#Binding var fscale: CGFloat
#Binding var foffset: CGSize
var body: some View {
innerContainerView(ia: $ia)
.frame(width: innerContainerSize.width, height: innerContainerSize.height)
.background(Color.yellow)
.scaleEffect(fscale, anchor: .init(x: (250 - foffset.width) / 500, y: (250 - foffset.height) / 500))
.offset(foffset)
}
}
struct ContentView: View {
#State var ia = [si]()
#State var fscale: CGFloat = 1
#State var foffset: CGSize = .zero
var body: some View {
VStack(alignment: .center) {
ContainerView(ia: $ia, fscale: $fscale, foffset: $foffset)
.frame(width: 500, height: 500)
.background(Color.blue)
.clipped()
.offset(x: 50)
}
}
}
you can use .alignmentGuide() with GeometryReader.
Possible example :
struct ContentView: View {
let color = [Color.red, .green, .yellow, .blue, .orange, .gray]
let number = [5,4,3,2,1]
var body: some View {
GeometryReader { proxy in
ZStack {
Rectangle()
.fill(.red)
.scaleEffect(CGSize(width: 3, height: 2))
.frame(width: 100,height: 100)
.border(Color.blue, width: 4)
Rectangle()
.frame(width: 250,height: 100)
.offset(x: 50, y: 75)
.alignmentGuide(HorizontalAlignment.center) { viewDimension in
viewDimension[HorizontalAlignment.center] + 50 // offset of x
}
.alignmentGuide(VerticalAlignment.center, computeValue: { viewDimension in
viewDimension[VerticalAlignment.center] + 75 // offset of y
})
.border(Color.gray, width: 4)
.position(x: proxy.size.width/2 - 50, y: proxy.size.height/2 - 75)
// adding this once is enough
Rectangle()
.fill(Color.yellow)
.frame(width: 50,height: 30)
.offset(x: 100, y: 150)
.alignmentGuide(HorizontalAlignment.center) { viewDimension in
viewDimension[HorizontalAlignment.center] + 100 // offset of x
}
.alignmentGuide(VerticalAlignment.center, computeValue: { viewDimension in
viewDimension[VerticalAlignment.center] + 150 // offset of y
})
.border(Color.gray, width: 4)
}
.border(.gray)
}
}
}
I'm trying to make such ruler in SwiftUI https://imgur.com/a/M89GXsj. I wonder is it possible to do it with only one ForEach? Is it right implementation?
struct Ruler: View {
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(1...20, id: \.self) { num in
HStack(spacing: 40) {
HStack(alignment: .top, spacing: 3) {
Divider()
.frame(width: 1.5, height: 40)
.background(Color.white)
Text("\(num)")
.foregroundColor(Color.white)
.font(.system(size: 12, weight: .bold))
}
ForEach(1...3, id: \.self) { _ in
Divider()
.frame(width: 1.5, height: 15)
.background(Color.white)
.padding(.bottom, -14)
}
Spacer()
}
}
}
.frame(height: 40)
.background(Color.black)
}
}
struct Ruler_Previews: PreviewProvider {
static var previews: some View {
Ruler()
.previewLayout(.sizeThatFits)
}}
My recommendation would be to have each portion of the ruler be a Shape with an associated enum to represent the tick marks needed. Each iteration of the shape would draw exactly 1 section of the ruler. By doing this, you gain more control and more precision as to how to draw it, with 1 ForEach and simple repeatability. The shape could be defined as:
struct Unit: Shape {
let num: Int
let ticks: [Tick]
func path(in rect: CGRect) -> Path {
let distance = rect.width / CGFloat(ticks.count)
var path = Path()
var x = rect.minX
for tick in ticks {
switch tick {
case .major:
path.move(to: CGPoint(x: x, y: rect.minY))
path.addLine(to: CGPoint(x: x, y: rect.maxY))
case .mid:
path.move(to: CGPoint(x: x, y: rect.maxY))
path.addLine(to: CGPoint(x: x, y: rect.maxY - 16))
case .minor:
path.move(to: CGPoint(x: x, y: rect.maxY))
path.addLine(to: CGPoint(x: x, y: rect.maxY - 8))
}
x += distance
print("tick")
}
return path
}
}
enum Tick {
case major, mid, minor
}
Then your view would simplify to this:
struct Ruler: View {
let ticks: [Tick] = [.major, .minor, .mid, .minor, .mid, .minor, .mid, .minor]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(1...20, id: \.self) { num in
Unit(num: num, ticks: ticks)
.stroke(.white, lineWidth: 2)
.frame(width: 100)
.overlay(
Text("\(num)")
.padding(.trailing, 2)
.foregroundColor(.white),
alignment: .topTrailing
)
}
}
.frame(height: 40)
.padding(.top, 2)
.padding(.bottom, 1)
.background(Color.black)
}
.padding()
}
}
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 have simple View which has 2 DragGesture, first one updating and second one onEnded, I want make my body cleaner and i want transfer those functions how we can do that? I think those are kind of clouser functions.
struct ContentView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
var body: some View {
Circle()
.fill()
.frame(width: 100, height: 100, alignment: .center)
.position(x: location.width + translation.width + 100, y: location.height + translation.height + 100)
.gesture(
DragGesture()
// transfer to updatingFunction
.updating($translation) { value, state, _ in
state = value.translation
}
// transfer to onEndedFunction
.onEnded { value in onEndedFunction(value: value)}
)
}
func updatingFunction() {
}
func onEndedFunction(value: DragGesture.Value) {
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
}
}
You can use inout for passing state.
struct ContentViewGesture: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
var body: some View {
Circle()
.fill()
.frame(width: 100, height: 100, alignment: .center)
.position(x: location.width + translation.width + 100, y: location.height + translation.height + 100)
.gesture(
DragGesture()
// transfer to updatingFunction
.updating($translation) { value, state, _ in
updatingFunction(value: value, state: &state) //<<-- Here
}
// transfer to onEndedFunction
.onEnded { value in onEndedFunction(value: value)}
)
}
func updatingFunction(value: DragGesture.Value, state: inout CGSize) { //<<-- Here
state = value.translation
}
func onEndedFunction(value: DragGesture.Value) {
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
}
}
It does not look much reasonable, instead I would recommend to separate entire gesture (if you look for code simplification, readability, etc.)
Here is possible variant:
struct ContentView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
private var dragGesture: some Gesture {
DragGesture()
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
}
}
var body: some View {
Circle()
.fill()
.frame(width: 100, height: 100, alignment: .center)
.position(x: location.width + translation.width + 100, y: location.height + translation.height + 100)
.gesture(dragGesture)
}
}