Imagine a Grid (n x n) squares. Those squares are ZStacks. It contains an optinal piece (In this case a circle). If I offset that piece over another ZStack it gets hidden by the other ZStack.
What I'm trying to do is a chess game. Imagine the Circle() being a piece.
This was my initial attemp:
struct ContentView: View {
#State var circle1Offset: CGSize = .zero
var body: some View {
VStack {
ZStack {
Color.blue
Circle().fill(Color.black)
.frame(width: 50, height: 50)
.offset(circle1Offset)
.gesture(
DragGesture()
.onChanged { value in
self.circle1Offset = value.translation
}
)
}
.frame(width: 150, height: 150)
ZStack {
Color.red
}
.frame(width: 150, height: 150)
}
}
}
Also I tried to add an overlay() instead of using a ZStack. Not sure which is more precise for this case, but unluckily i can't add an "optional" overlay like so:
struct ContentView: View {
#State var circle1Offset: CGSize = .zero
var body: some View {
VStack {
Color.blue
.frame(width: 150, height: 150)
.overlay(
Circle().fill(Color.black)
.frame(width: 50, height: 50)
.offset(circle1Offset)
.gesture(
DragGesture()
.onChanged { value in
self.circle1Offset = value.translation
}
)
)
Color.red
.frame(width: 150, height: 150)
}
}
func tileHasPiece() -> Circle? {
return Circle() // This would consult my model
}
}
But as I said, I don't know how to use tileHasPiece() to add an overlay depending on this.
Just put all board static elements below, and all figure active elements above, as in below modified your code snapshot. In such case everthing will be in one coordinate space. (Of course calculation of coordinates for figures is out of this topic)...
struct FTContentView: View {
#State var circle1Offset: CGSize = .zero
var body: some View {
ZStack {
// put board below
VStack {
Color.blue
.frame(width: 150, height: 150)
Color.red
.frame(width: 150, height: 150)
}
// put figure above
Circle().fill(Color.black)
.frame(width: 50, height: 50)
.offset(circle1Offset)
.gesture(
DragGesture()
.onChanged { value in
self.circle1Offset = value.translation
}
)
} // board coordinate space
}
}
Related
So, I saw many swipe to buy buttons around the web and on many apps. I thought it would be good to implement onto my app. But for some reason, the ZStack isn't functioning properly, the background is on top of the swipe arrow. I can't properly place it inside of another view. Furthermore, how would one perform an action when the swipable arrow is fully at the end?
import SwiftUI
struct ContentView: View {
#State var viewState = CGSize(width: 0, height: 50)
var body: some View {
ZStack{
/// This part is the background of the swipe to buy
RoundedRectangle(cornerRadius: 10)
.offset(x: viewState.width)
.frame(width: 800,height: 100)
HStack{
// This part is the actual swiping arrow
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(x: viewState.width)
.gesture(
DragGesture().onChanged { value in
viewState = value.translation
}
.onEnded { value in
withAnimation(.spring()) {
viewState = .zero
}
}
)
Image(systemName: "chevron.right")
.offset(x: viewState.width)
.gesture(
DragGesture().onChanged { value in
viewState = value.translation
}
.onEnded { value in
withAnimation(.spring()) {
viewState = .zero
}
}
)
}
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
'''
Your ZStack is working fine, the issue is you are giving the top RoundedRectangle fix width = 800 and you use Spacer() to push the HStack to the leading side. You have to remove this width in order to work this fine, like this:
RoundedRectangle(cornerRadius: 10)
.offset(x: viewState.width)
.frame(height: 100)
But if you want fix width you can use background() instead of ZStack, like this:
HStack{
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(x: viewState.width)
.gesture(
DragGesture().onChanged { value in
viewState = value.translation
}
.onEnded { value in
withAnimation(.spring()) {
viewState = .zero
}
}
)
Image(systemName: "chevron.right")
.offset(x: viewState.width)
.gesture(
DragGesture().onChanged { value in
viewState = value.translation
}
.onEnded { value in
withAnimation(.spring()) {
viewState = .zero
}
}
)
}
Spacer()
}.background(
RoundedRectangle(cornerRadius: 10)
.offset(x: viewState.width)
.frame(width: 800,height: 100)
)
Help me please to catch how to draw circles on random position in view borders.
Red View options
.background(Color.red)
.padding(40)
Circles:
var body: some View
{
Circle()
.strokeBorder(Color.blue, lineWidth: 2.0, antialiased: true)
.frame(width: 50, height: 50, alignment: .center)
.overlay(Text(title))
}
I try to add circles on red view in random position using .position option, but you can see its not working:
The main problem is how to know width and height of red view on any device and set .position of circles.
You can use GeometryReader for reading the size of View, like in the example:
struct ContentView: View {
var body: some View {
GeometryReader { proxy in
ForEach(0...50, id:\.self) { index in
CirclesView(index: index)
.offset(x: CGFloat.random(in: 0.0...proxy.size.width), y: CGFloat.random(in: 0.0...proxy.size.height))
}
}
.background(Color.red)
.ignoresSafeArea()
}
}
struct CirclesView: View {
let index: Int
var body: some View {
Circle()
.strokeBorder(Color.blue, lineWidth: 2.0, antialiased: true)
.frame(width: 50, height: 50, alignment: .center)
.overlay(Text(String(describing: index)))
}
}
Update:
struct ContentView: View {
var body: some View {
GeometryReader { proxy in
ForEach(0...50, id:\.self) { index in
CirclesView(index: index, offset: logicalFunction(size: proxy.size))
}
}
.background(Color.red)
.ignoresSafeArea()
}
func logicalFunction(size: CGSize) -> CGSize {
// Do your works here!
let width: CGFloat = CGFloat.random(in: 0.0...size.width)
let height: CGFloat = CGFloat.random(in: 0.0...size.height)
return CGSize(width: width, height: height)
}
}
struct CirclesView: View {
let index: Int
let offset: CGSize
var body: some View {
Circle()
.strokeBorder(Color.blue, lineWidth: 2.0, antialiased: true)
.frame(width: 50, height: 50, alignment: .center)
.overlay(Text(String(describing: index)))
.offset(offset)
}
}
I have a SwiftUI application. It has a ScrollView and a Text and I want the Text to display the position of the elements in the ScrollView.
struct LinkedScrolling: View {
#State var scrollPosition: CGFloat = 0
var body: some View {
VStack {
Text("\(self.scrollPosition)")
ScrollContent(scrollPosition: self.$scrollPosition)
}
}
}
This View contains the Text and the ScrollContent. This is ScrollContent:
struct ScrollContent: View {
#Binding var scrollPosition: CGFloat
var body: some View {
ScrollView(.horizontal) {
GeometryReader { geometry -> AnyView in
self.scrollPosition = geometry.frame(in: .global).minX
let view = AnyView(HStack(spacing: 20) {
Rectangle()
.fill(Color.blue)
.frame(width: 400, height: 200)
Rectangle()
.fill(Color.red)
.frame(width: 400, height: 200)
Rectangle()
.fill(Color.green)
.frame(width: 400, height: 200)
Rectangle()
.fill(Color.orange)
.frame(width: 400, height: 200)
Rectangle()
.fill(Color.pink)
.frame(width: 400, height: 200)
})
return view
}.frame(width: 5*400+4*20)
}
}
}
The State variable scrollPosition gets updated every time the elements in the ScrollView move.
When using the app in an iOS 14.2 Simulator, scrollPosition does not change and the console logs [SwiftUI] Modifying state during view update, this will cause undefined behavior..
What really confuses me, is that it works in the Xcode preview canvas and the State variable and the Text change like I want them to.
Is it possible to change the State variable this way?
If yes, how can I try to make it work on the Simulator?
If no, is there any other way to achieve my goal?
Thank you for your help!
You can use DispatchQueue.main.async.
DispatchQueue.main.async {
self.scrollPosition = geometry.frame(in: .global).minX
}
Or better way is to use .onReceive
Like this
struct ScrollContent: View {
#Binding var scrollPosition: CGFloat
var body: some View {
ScrollView(.horizontal) {
GeometryReader { geometry in
HStack(spacing: 20) {
Rectangle()
.fill(Color.blue)
.frame(width: 400, height: 200)
Rectangle()
.fill(Color.red)
.frame(width: 400, height: 200)
Rectangle()
.fill(Color.green)
.frame(width: 400, height: 200)
Rectangle()
.fill(Color.orange)
.frame(width: 400, height: 200)
Rectangle()
.fill(Color.pink)
.frame(width: 400, height: 200)
}.onReceive(Just(geometry), perform: { _ in //<-- Here
self.scrollPosition = geometry.frame(in: .global).minX
})
}.frame(width: 5*400+4*20)
}
}
}
I'm trying to achieve something that is quite easy in UIKit - one view that is always in in the center (image) and the second view (text) is on top of it with some spacing between two views. I tried many different approaches (mainly using alignmentGuide but nothing worked as I'd like).
code:
ZStack {
Rectangle()
.foregroundColor(Color.red)
VStack {
Text("Test")
.padding([.bottom], 20) // I want to define spacing between two views
Image(systemName: "circle")
.resizable()
.alignmentGuide(VerticalAlignment.center, computeValue: { value in
value[VerticalAlignment.center] + value.height
})
.frame(width: 20, height: 20)
}
}
.frame(width: 100, height: 100)
result:
As you can see image is not perfectly centered and it actually depends on the padding value of the Text. Is there any way to force vertical and horizontal alignment to be centered in the superview and layout second view without affecting centered view?
I think the “correct” way to do this is to define a custom alignment:
extension VerticalAlignment {
static var custom: VerticalAlignment {
struct CustomAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.center]
}
}
return .init(CustomAlignment.self)
}
}
Then, tell your ZStack to use the custom alignment, and use alignmentGuide to explicitly set the custom alignment on your circle:
PlaygroundPage.current.setLiveView(
ZStack(alignment: .init(horizontal: .center, vertical: .custom)) {
Color.white
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
VStack {
Text("Test")
Circle()
.stroke(Color.white)
.frame(width: 20, height: 20)
.alignmentGuide(.custom, computeValue: { $0.height / 2 })
}
}
.frame(width: 300, height: 300)
)
Result:
You can center the Image by moving it to ZStack. Then apply .alignmentGuide to the Text:
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.red)
Text("Test")
.alignmentGuide(VerticalAlignment.center) { $0[.bottom] + $0.height }
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
}
.frame(width: 100, height: 100)
}
}
Note that as you specify the width/height of the Image explicitly:
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
you can specify the .alignmentGuide explicitly as well:
.alignmentGuide(VerticalAlignment.center) { $0[.bottom] + 50 }
Here is possible alternate, using automatic space consuming feature
Tested with Xcode 12 / iOS 14
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.red)
VStack(spacing: 0) {
Color.clear
.overlay(
Text("Test").padding([.bottom], 10),
alignment: .bottom)
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
Color.clear
}
}
.frame(width: 100, height: 100)
}
}
Note: before I used Spacer() for such purpose but with Swift 2.0 it appears spacer becomes always just a spacer, ie. nothing can be attached to it - maybe bug.
My goal is to make sure Text in a container to scale according to its parent. It works well when the container only contains one Text view, as following:
import SwiftUI
struct FontScalingExperiment: View {
var body: some View {
Text("Hello World ~!")
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.scaledToFill()
)
}
}
struct FontScalingExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
FontScalingExperiment()
.previewLayout(.fixed(width: 100, height: 100))
FontScalingExperiment()
.previewLayout(.fixed(width: 200, height: 200))
FontScalingExperiment()
.previewLayout(.fixed(width: 300, height: 300))
FontScalingExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
the result:
However, when we have more complex View, we cant use same approach to automatically scale the text based on its parent size, for example:
import SwiftUI
struct IndicatorExperiment: View {
var body: some View {
VStack {
HStack {
Text("Line 1")
Spacer()
}
Spacer()
VStack {
Text("Line 2")
Text("Line 3")
}
Spacer()
Text("Line 4")
}
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
}
}
struct IndicatorExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
IndicatorExperiment()
.previewLayout(.fixed(width: 100, height: 100))
IndicatorExperiment()
.previewLayout(.fixed(width: 200, height: 200))
IndicatorExperiment()
.previewLayout(.fixed(width: 300, height: 300))
IndicatorExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
Simply adding these 3 modifiers:
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
wont produce result like the first example; Text enlarged beyond the frame.
I did successfully, produce the result that I want by using GeometryReader then scale the font size based on geometry.size.width. Is this the only approach for achieving the desired result in SwiftUI?
You can try make all the Texts the same height. To do this you will need to set the padding and spacing explicitly, so this will scale rather than the fixed default values.
Also, the Spacer() didn't make much sense here - if the requirement was that all the Text stay the same size, the Spacer would just make all the text small. For Text to scale based on space, and where Spacer tries to use as much space as possible, it's a contradiction. Instead, I decided to just set the VStack's spacing in the initializer.
Working code:
struct IndicatorExperiment: View {
private let size: CGFloat
private let padding: CGFloat
private let primarySpacing: CGFloat
private let secondarySpacing: CGFloat
private let textHeight: CGFloat
init(size: CGFloat) {
self.size = size
padding = size / 10
primarySpacing = size / 15
secondarySpacing = size / 40
let totalHeights = size - padding * 2 - primarySpacing * 2 - secondarySpacing
textHeight = totalHeights / 4
}
var body: some View {
VStack(spacing: primarySpacing) {
HStack {
scaledText("Line 1")
Spacer()
}
.frame(height: textHeight)
VStack(spacing: secondarySpacing) {
scaledText("Line 2")
scaledText("Line 3")
}
.frame(height: textHeight * 2 + secondarySpacing)
scaledText("Line 4")
}
.padding(padding)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
.frame(width: size, height: size)
}
private func scaledText(_ content: String) -> some View {
Text(content)
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(height: textHeight)
}
}
Code to test with:
struct ContentView: View {
var body: some View {
ScrollView {
VStack(spacing: 50) {
IndicatorExperiment(size: 100)
IndicatorExperiment(size: 200)
IndicatorExperiment(size: 300)
IndicatorExperiment(size: 400)
}
}
}
}
Result:
Using GeometryReader and a .minimumScaleFactor modifier would probably the only way to scale text in a view. To have more control on sizing, one possible way is to provde the .frame size from the parent view.
Scalable Text View
GeometryReader { geo in
Text("Foo")
.font(
.system(size: min(geo.size.height, geo.size.width) * 0.95))
.minimumScaleFactor(0.05)
.lineLimit(1)
}
Parent View that uses the Scalable Text View
GeometryReader { geo in
ScaleableText()
.frame(width: geo.size.width, height: geo.size.height)
}