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)
}
}
}
Related
How can I hide my arrow text after ScrollView has scrolled?
struct Skroll: View {
var body: some View {
VStack(alignment: .trailing) {
Text("<-")
.font(.system(size: 25).bold())
.kerning(-3)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Rectangle()
.frame(width: 200, height: 300)
.cornerRadius(20)
Rectangle()
.frame(width: 200, height: 300)
.cornerRadius(20)
Rectangle()
.frame(width: 200, height: 300)
.cornerRadius(20)
}
}
}
.padding()
}
}
I can't figure out how can I hide text after scrolling, I'm new and just learning SwiftUI
Looks like what you need is to get the current position of the scroll view. See here on how to do that. Then you can choose to display Text("<-") based on a flag which is modified when ScollView reaches a certain point
if !hideText {
Text("<-")
.font(.system(size: 25).bold())
.kerning(-3)
}
It might be also possible that you might achieve the same result by moving your Text("<-") inside the scroll view. See if below works for you
ScrollView(.horizontal, showsIndicators: false) {
Text("<-")
.font(.system(size: 25).bold())
.kerning(-3)
HStack {
Rectangle()
.frame(width: 200, height: 300)
.cornerRadius(20)
Rectangle()
.frame(width: 200, height: 300)
.cornerRadius(20)
Rectangle()
.frame(width: 200, height: 300)
.cornerRadius(20)
}
}
I think I figured out how to do it
struct Skroll: View {
#State var scrollViewOffset: CGFloat = 0
#State var start: CGFloat = 0
var body: some View {
VStack(alignment: .trailing) {
HStack {
Image(systemName: "arrowtriangle.right.fill")
.font(.system(size: 35).bold())
.opacity(-scrollViewOffset > 160.0 ? 1 : 0)
.animation(.easeOut, value: scrollViewOffset)
Spacer()
Image(systemName: "arrowtriangle.left.fill")
.font(.system(size: 35))
.opacity(-scrollViewOffset > 160.0 ? 0 : 1)
.animation(.easeOut, value: scrollViewOffset)
}
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Rectangle()
.frame(width: 200, height: 300)
.cornerRadius(20)
Rectangle()
.frame(width: 200, height: 300)
.cornerRadius(20)
Rectangle()
.frame(width: 200, height: 300)
.cornerRadius(20)
}
.overlay(GeometryReader { proxy -> Color in
DispatchQueue.main.async {
if start == 0 {
self.start = proxy.frame(in: .global).minX
}
let offset = proxy.frame(in: .global).minX
self.scrollViewOffset = offset - start
print(self.scrollViewOffset)
}
return Color.clear
})
}
}
.padding()
}
}
result
I replaced my text with an image. I'm not sure if this is the right solution and I don't know if it might cause any errors, but this works for me. Maybe someone will find it useful too
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.
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
}
}
import SwiftUI
struct ContentView: View {
#State private var isActive : Bool = false
var body: some View {
VStack {
Image("home-img")
.resizable()
.aspectRatio(contentMode:.fill)
.edgesIgnoringSafeArea(.top)
.edgesIgnoringSafeArea(.bottom)
.overlay(Image("round")
.resizable()
.frame(width: 350, height: 350 , alignment: .center)
.overlay(Image("girl-img")
.resizable()
.frame(width: 150, height: 150, alignment: .center)
)
.overlay(Image("video")
.resizable()
.frame(width: 100, height: 100)
.offset(y: -200)
.padding(.bottom, -70)
).onTapGesture(count: 1) {
NavigationView {
NavigationLink(destination: SelecteImageView(), isActive: self.$isActive) {
Text("")
}
}
}
}
}
}
What I want to do is when I tap on Image("Video") it should redirect me to a new screen using NavigationView. Normally, with button, it redirects to a new screen, but here the problem is that the image is in overlay. Thank you in advance.
You need to put your Image Views inside the NavigationLink label.
The navigationLink acts like Button and it gets the default button style with blue color. So change the default button style to plain using the bellow modifier:
import SwiftUI
struct ContentView: View {
#State private var isActive : Bool = false
var body: some View {
NavigationView {
NavigationLink(destination: SelecteImageView(), isActive: self.$isActive) {
VStack {
Image("home-img") //.renderingMode(.original)
.resizable()
.aspectRatio(contentMode:.fill)
.edgesIgnoringSafeArea(.top)
.edgesIgnoringSafeArea(.bottom)
.clipped() //Here shows your Image only inside the frame
.overlay(Image("round") //.renderingMode(.original)
.resizable()
.frame(width: 350, height: 350 , alignment: .center))
.overlay(Image("girl-img") //.renderingMode(.original)
.resizable()
.frame(width: 150, height: 150, alignment: .center)
)
.overlay(Image("video") //.renderingMode(.original)
.resizable()
.frame(width: 100, height: 100)
.offset(y: -200)
.padding(.bottom, -70)
)
}
}.buttonStyle(PlainButtonStyle())
}
}
}
Or, you can give your Image views .renderingMode(.original) modifier if you are only using Images, and remove .buttonStyle(PlainButtonStyle()) from above code.
Please add tap gesture over here
struct Contentview: View {
#State var isNavigate :Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: LoginView()) {
Image("Birthday")
.resizable()
.aspectRatio(contentMode:.fill)
.edgesIgnoringSafeArea(.top)
.edgesIgnoringSafeArea(.bottom)
.frame(width: 200, height: 200 , alignment: .center)
.overlay(Image("round")
.resizable()
.frame(width: 350, height: 350 , alignment: .center)
)
.overlay(Image("girl-img")
.resizable()
.frame(width: 150, height: 150, alignment: .center)
)
}
}
}
}
}
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)
}