This sample code illustrates my problem:
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
subviewone()
subviewone()
subviewone()
subviewone()
subviewone()
subviewone()
graph()
}.padding(.all)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct graph: View {
var body: some View {
GeometryReader { geo in
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: geo.size.width, y: 0))
path.addLine(to: CGPoint(x: geo.size.width, y: geo.size.width))
}.fill(Color.red)
}
}
}
struct subviewone: View {
var body: some View {
VStack {
Text("testtest")
Text("testtest")
Text("testtest")
Text("testtest")
Text("testtest")
Text("testtest")
Text("testtest")
}
}
}
I need one view to display some graph, and I need the size of the space allocated for the graph view to scale the axis.
That is why I placed it inside GeometryReader.
When I run this code on a device that would need to scroll (iPhone 8 for example), I am unable to scroll to see the whole "graph".
It looks as if the "graph view" is somehow drawn outside of the ScrollView.
Setting frame on the Path view doesn't help.
Thank you for any help provided.
1) you should use Shape, because then you get a rect in which you can "paint"
2) you have to give a frame with it, because Apple does not know how big your shape is (see the Rectangles i appended at the end)
3) if you do not give a frame with it, you get a "default" height for your shape, as Rectangles get it
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
subviewone()
subviewone()
subviewone()
subviewone()
subviewone()
subviewone()
graph()
.fill(Color.red)
.frame(height:400) // or: .aspectRatio(contentMode: .fill)
Rectangle()
Rectangle()
Rectangle()
}.padding(.all)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct graph: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.size.width, y: 0))
path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.width))
}
}
}
struct subviewone: View {
var body: some View {
VStack {
Text("testtest")
Text("testtest")
Text("testtest")
Text("testtest")
Text("testtest")
Text("testtest")
Text("testtest")
}
}
}
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 🎉
I have a view in SwiftUI. This view has some random images on it in various random positions. Check the code below.
struct ContentView: View {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
ForEach(0..<5) { _ in
Image(systemName: "plus")
.frame(width: 30, height: 30)
.background(Color.green)
.position(
x: CGFloat.random(in: 0..<screenWidth),
y: CGFloat.random(in: 0..<screenHeight)
)
}
}
.ignoreSafeArea()
}
}
I need to get the exact position of these random added images and pass the positions to another transparent view that shows up with a ZStack on top of the previous view. In the transparent popup fullscreen ZStack view i need to point to the position of the images i randomly put in the previous view using arrow images. Is this somehow possible in swiftui? I am new in swiftui so any help or suggestion appreciated.
Store the random offsets in a #State var and generate them in .onAppear { }. Then you can use them to position the random images and pass the offsets to the overlay view:
struct ContentView: View {
#State private var imageOffsets: [CGPoint] = Array(repeating: CGPoint.zero, count: 5)
#State private var showingOverlay = true
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
ForEach(0..<5) { index in
Image(systemName: "plus")
.frame(width: 30, height: 30)
.background(Color.green)
.position(
x: imageOffsets[index].x,
y: imageOffsets[index].y
)
}
}
.ignoresSafeArea()
.onAppear {
for index in 0..<5 {
imageOffsets[index] = CGPoint(x: .random(in: 0..<screenWidth), y: .random(in: 0..<screenHeight))
}
}
.overlay {
if showingOverlay {
OverlayView(imageOffsets: imageOffsets)
}
}
}
}
struct OverlayView: View {
let imageOffsets: [CGPoint]
var body: some View {
ZStack {
Color.clear
ForEach(0..<5) { index in
Circle()
.stroke(.blue)
.frame(width: 40, height: 40)
.position(
x: imageOffsets[index].x,
y: imageOffsets[index].y
)
}
}
.ignoresSafeArea()
}
}
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)
}
}
TL;DR - I would like to add a Path or a Shape ? to an existing Shape and would like to know if I can somehow use path.addPath(...) See image for a visual of what I mean
I posted a question earlier about this but don't think I phrased it well and very possibly went about it the wrong way. All I would like to do is to add shapes to an existing shape. I see that Path has a method addPath(:) that lets us add one path to another.. and that would be perfect if I could figure out where to do it at. Here is my current attempt:
import SwiftUI
struct ContentView: View {
#State private var path = Path()
var body: some View {
VStack {
MyShape()
.stroke(lineWidth: 3)
.padding()
HStack {
AddHorizontalLineView()
AddVerticalLineView()
AddSquareView()
AddDiagonalLineView()
}
}
}
private func addBoxToPath(_ path: Path) -> Path { <-- No where to call this
var workingPath = path
workingPath.addRect(workingPath.boundingRect)
return workingPath
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct MyShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.addRect(rect)
return path
}
}
struct AddHorizontalLineView: View {
var body: some View {
Button(action: {}){
Rectangle()
.stroke()
.overlay(
HorizontalWire()
.stroke()
.padding()
)
}.frame(width: 50, height: 50)
}
}
struct AddVerticalLineView: View {
var body: some View {
Button(action: { }){
Rectangle()
.stroke()
.overlay(
VerticalWire()
.stroke()
.padding()
)
}.frame(width: 50, height: 50)
}
}
struct AddSquareView: View {
var body: some View {
Button(action: {
}){
Rectangle()
.stroke()
.overlay(
Rectangle()
.stroke()
.padding()
)
}.frame(width: 50, height: 50)
}
}
struct AddDiagonalLineView: View {
var body: some View {
Button(action: {}){
Rectangle()
.stroke()
.overlay(
DiagonalWire()
.stroke()
.foregroundColor(.red)
.padding()
)
}
.frame(width: 50, height: 50)
}
}
struct DiagonalWire: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
return path
}
}
struct VerticalWire: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
return path
}
}
struct HorizontalWire: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
return path
}
}
Can paths be added dynamically like this or the entire setup needs to be done beforehand in path(in:) method of every type that conforms to Shape
I hope my question makes more sense now..
You can pass the Path into MyShape (although it really has very little point then -- you could just render the Path itself).
Here's a simple example (with some of your buttons removed since it'll be a very amount of work to hook up all of the logic). I also had to add an additional rect to the Path to start with, since your addBoxToPath depends on there being something in the Path so that the boundingBox isn't just a 0x0 rect:
struct ContentView: View {
#State private var path = Path()
var body: some View {
VStack {
MyShape(path: path)
.stroke(lineWidth: 3)
.padding()
HStack {
Button(action: {
path = addBoxToPath(path)
}) {
Text("add")
}
}
}.onAppear {
path.addRect(CGRect(origin: .zero, size: CGSize(width: 200, height: 200)))
}
}
private func addBoxToPath(_ path: Path) -> Path {
var workingPath = path
workingPath.addRect(workingPath.boundingRect.insetBy(dx: -5, dy: -5))
return workingPath
}
}
struct MyShape: Shape {
var path : Path
func path(in rect: CGRect) -> Path {
return path
}
}
I'm trying to create a custom percentage bar chart like the one below
however, I'm unable to set the bar width programmatically, what am I doing wrong?
Setting .frame(width: UIScreen.main.bounds.width * percent, height:23) produces the following error : Ambiguous reference to member 'frame(width:height:alignment:)'
import SwiftUI
struct BarChartView : View {
#Binding var percent: Float
init(percentage: Binding<Float>){
self._percent = percentage
}
var body: some View {
ZStack {
Rectangle().fill(Color.yellow).frame(height: 30).border(Color.black, width:1)
HStack {
RoundedRectangle(cornerRadius: 5)
.fill(Color.green).frame(width: 300, height:23).padding(2)
Spacer()
}
HStack {
Text("Bar Chart View").padding (2)
Spacer()
Text("\(String(format: "%02.f", arguments: [self.percent]))%")
}
}
}
}
Is there a way to determine the width of the first rectangle in the ZStack and calc a percentage off of that. I would like this to automatically update in landscape mode too, if possible.
You could use GeometryReader to play with dimensions, but in this case I think using a shape is a much better fit:
import SwiftUI
struct BarChartView : View {
#Binding var percent: Float
init(percentage: Binding<Float>){
self._percent = percentage
}
var body: some View {
HStack {
Text("Bar Chart View").padding (5)
Spacer()
Text("\(String(format: "%02.f", arguments: [self.percent * 100]))%").padding(5)
}
.background(LeftPart(pct: CGFloat(percent)).fill(Color.green))
.background(RightPart(pct: CGFloat(percent)).fill(Color.yellow))
.padding(10)
}
struct LeftPart: Shape {
let pct: CGFloat
func path(in rect: CGRect) -> Path {
var p = Path()
p.addRoundedRect(in: CGRect(x: 0, y: 0, width: rect.size.width * pct, height: rect.size.height), cornerSize: .zero)
return p
}
}
struct RightPart: Shape {
let pct: CGFloat
func path(in rect: CGRect) -> Path {
var p = Path()
p.addRoundedRect(in: CGRect(x: rect.size.width * pct, y: 0, width: rect.size.width * (1-pct), height: rect.size.height), cornerSize: .zero)
return p
}
}
}
struct ContentView: View {
var body: some View {
BarChartView(percentage: .constant(0.75))
}
}