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 🎉
Related
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 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've created this masked blur effect (code below), it runs in SwiftUI, but uses UIViewRepresentable for the masking, is it possible to re-create the same effect, but just in pure SwiftUI?
Here's the current code, if you run it, use your finger to drag on the screen, this moves the mask to reveal underneath.
import SwiftUI
import UIKit
struct TestView: View {
#State var position: CGPoint = .zero
var simpleDrag: some Gesture {
DragGesture()
.onChanged { value in
self.position = value.location
}
}
var body: some View {
ZStack {
Circle()
.fill(Color.green)
.frame(height: 200)
Circle()
.fill(Color.pink)
.frame(height: 200)
.offset(x: 50, y: 100)
Circle()
.fill(Color.orange)
.frame(height: 100)
.offset(x: -50, y: 00)
BlurView(style: .light, position: $position)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.gesture(
simpleDrag
)
}
}
struct BlurView: UIViewRepresentable {
let style: UIBlurEffect.Style
#Binding var position: CGPoint
func makeUIView(context: UIViewRepresentableContext<BlurView>) -> UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
let blurEffect = UIBlurEffect(style: style)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(blurView, at: 0)
NSLayoutConstraint.activate([
blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
blurView.widthAnchor.constraint(equalTo: view.widthAnchor),
])
let clipPath = UIBezierPath(rect: UIScreen.main.bounds)
let circlePath = UIBezierPath(ovalIn: CGRect(x: 100, y: 0, width: 200, height: 200))
clipPath.append(circlePath)
let layer = CAShapeLayer()
layer.path = clipPath.cgPath
layer.fillRule = .evenOdd
view.layer.mask = layer
view.layer.masksToBounds = true
return view
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<BlurView>) {
let clipPath = UIBezierPath(rect: UIScreen.main.bounds)
let circlePath = UIBezierPath(ovalIn: CGRect(x: position.x, y: position.y, width: 200, height: 200))
clipPath.append(circlePath)
let layer = CAShapeLayer()
layer.path = clipPath.cgPath
layer.fillRule = .evenOdd
uiView.layer.mask = layer
uiView.layer.masksToBounds = true
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
I think I nearly have a solution, I can use a viewmodifer to render the result twice on top of each other with a ZStack, I can blur one view, and use a mask to knock a hole in it.
import SwiftUI
struct TestView2: View {
#State var position: CGPoint = .zero
var simpleDrag: some Gesture {
DragGesture()
.onChanged { value in
self.position = value.location
}
}
var body: some View {
ZStack {
Circle()
.fill(Color.green)
.frame(height: 200)
Circle()
.fill(Color.pink)
.frame(height: 200)
.offset(x: 50, y: 100)
Circle()
.fill(Color.orange)
.frame(height: 100)
.offset(x: -50, y: 00)
}
.maskedBlur(position: $position)
.gesture(
simpleDrag
)
}
}
struct MaskedBlur: ViewModifier {
#Binding var position: CGPoint
/// Render the content twice
func body(content: Content) -> some View {
ZStack {
content
content
.blur(radius: 10)
.mask(
Hole(position: $position)
.fill(style: FillStyle(eoFill: true))
.frame(maxWidth: .infinity, maxHeight: .infinity)
)
}
}
}
extension View {
func maskedBlur(position: Binding<CGPoint>) -> some View {
self.modifier(MaskedBlur(position: position))
}
}
struct Hole: Shape {
#Binding var position: CGPoint
func path(in rect: CGRect) -> Path {
var path = Path()
path.addRect(UIScreen.main.bounds)
path.addEllipse(in: CGRect(x: position.x, y: position.y, width: 200, height: 200))
return path
}
}
#if DEBUG
struct TestView2_Previews: PreviewProvider {
static var previews: some View {
TestView2()
}
}
#endif
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
}
}
}