Shape resizing animation on orientation change - swiftui

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.

Related

Draw Path depending on currently visible views

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 🎉

SwiftUI: How to center a rectangle relative to another view that has a custom anchor scaling effect?

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

How to make ruler in SwiftUI

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

How to center a graph using ScrollView - Scrolling by Pixel

I am using a ScrollView to show a Graph in order to horizontally scroll it. Problem is that I need to center it when the view initially loads.
struct ContentView1: View {
var body: some View {
VStack {
GeometryReader { geometry in
ScrollView(.horizontal) {
VStack {
Graph()
.frame(width: geometry.size.width * 2, height: geometry.size.height, alignment: .center)
}
}
}
}
.frame(minWidth: 400, minHeight: 300, alignment: .center)
}
}
fileprivate struct Graph: View {
var body: some View {
GeometryReader { geometry in
let rect = geometry.size
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
}
.stroke(Color.black)
Path { path in
path.move(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: 0, y: rect.height))
}
.stroke(Color.red)
}
}
}
ScrollViewReader provides the scrollTo function but it works if the ScrollView contains more views with their own identifier so it's not an option in this case.
How can I scroll the graph by pixel in order to center it programmatically?
(I think) Using ScrollView is unnecessary since you have only one View. You may use DragGesture to move Graph() on the x-axis. It also helps you center Graph() on the x-axis when it appears.
1.
Offset Graph() by half of the screen's width. In order to do so, define a new #State property, set it to -(geometry.size.width / 2), and use .offset() view-modifier.
2.
Use DragGesture() to move on the x-axis. You need to keep track of the last offset too.
struct ContentView: View {
#State var offset: CGFloat = .zero
#State var lastOffset: CGFloat = .zero
var body: some View {
VStack {
GeometryReader { geometry in
VStack {
Graph()
.frame(width: geometry.size.width * 2, height: geometry.size.height, alignment: .center)
.offset(x: offset)
}
.onAppear {
offset = -(geometry.size.width / 2)
lastOffset = offset
}
.gesture(
DragGesture().onChanged { value in
offset = lastOffset + value.translation.width
}
.onEnded { value in
lastOffset = offset
}
)
}
}
.frame(minWidth: 400, minHeight: 300, alignment: .center)
}
}

How to draw a line in SwiftUI with the mouse?

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