I have a ScrollView, when I scroll up, the content on the screen would move down. But I wanna limit how far the content can move down. I don't want the user to be able to scroll beyond the gray rectangle at the top. In other words, I don't want the user to see the white area above the gray rectangle when they scroll up. How can I achieve this in SwiftUI?
import SwiftUI
struct ScrollViewIssue: View {
var body: some View {
ScrollView {
VStack {
Rectangle()
.foregroundColor(.gray)
.frame(height: 200)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
}
}.ignoresSafeArea()
}
}
struct ScrollViewIssue: View {
var body: some View {
ScrollView {
VStack {
Rectangle()
.foregroundColor(.gray)
.frame(height: UIScreen.main.bounds.height)
.frame(height: 200, alignment: .bottom)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
}
}.ignoresSafeArea()
}
}
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
iam unsing a UiKit Image picker and trying to setup an Profil Pic for the User. Everything's works fine, the image gets displayed. But iam trying to make it into a Circle. I Tried using clipShape with an circle but it doesn't work. i followed the tutorial "My Images 1: Photo Picker and Camera in SwiftUI"
struct ProfileView: View {
#EnvironmentObject var appUser: User
#EnvironmentObject var appInfo: AppInformation
var body: some View {
ZStack {
Rectangle()
.frame(width: 400, height: 720)
.cornerRadius(50)
.foregroundColor(.gray)
.overlay(
HStack {
if let image = appUser.profilBild {
Image(uiImage: image)
.resizable()
.frame(width: 100, height: 100)
.scaledToFill()
.clipShape(RoundedRectangle(cornerRadius: 15))
.overlay(Circle().stroke(Color.orange, lineWidth: 10))
.onTapGesture {
appInfo.source = .library
appInfo.showPicker = true
}
.padding(20)
.shadow(radius: 10)
.overlay(
ZStack{
Rectangle()
.frame(width: 20, height: 20)
.offset(x: 35, y: -35)
.foregroundColor(.white)
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.offset(x: 35, y: -35)
.foregroundColor(.blue)
.shadow(radius: 10)
}
)
}
else {
Image(systemName: "person.circle")
.resizable()
.frame(width: 100, height: 100)
.onTapGesture {
appInfo.source = .library
appInfo.showPicker = true
}
.padding(20)
.shadow(radius: 10)
.overlay(
ZStack{
Rectangle()
.frame(width: 20, height: 20)
.offset(x: 35, y: -35)
.foregroundColor(.white)
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.offset(x: 35, y: -35)
.foregroundColor(.blue)
.shadow(radius: 10)
}
)
}
VStack {
Text(appUser.username)
.font(.largeTitle)
.frame(width: 240 ,alignment: .leading)
.offset(x: -10, y: -25)
.lineLimit(1)
Text(appUser.name)
.frame(width: 220, alignment: .leading)
.offset(x: -15,y: -20)
.lineLimit(1)
}
}
.frame(width: 400, height: 720, alignment: .topLeading)
)
.padding()
ZStack {
Rectangle()
.foregroundColor(.secondary)
.frame(width: 380, height: 510)
.cornerRadius(45)
}
.frame(width: 400, height: 700, alignment: .bottom)
.padding()
}
.sheet(isPresented: $appInfo.showPicker) {
ImagePicker(sourceType: appInfo.source == .library ? .photoLibrary : .camera, selectedImage: $appUser.profilBild)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}
You were almost there :)
Your only mistake was the order you placed your modifiers. ORDER MATTERS!!
Place scaledToFill() and clipShape() before the frame modifier.
Like such:
.resizable()
.scaledToFill()
.clipShape(Circle())
.frame(width: size, height: size)
My code:
public var body: some View {
ZStack(alignment: .bottom) {
Ellipse()
.fill(.yellow)
Text("Text")
.padding(.bottom, 42)
.foregroundColor(.red)
}
.frame(width: 546, height: 364)
.position(x: UIScreen.main.bounds.size.width / 2, y: Spacing.padding_0_5)
.edgesIgnoringSafeArea(.top)
.background(Color.red)
}
makes ZStack takes almost all screen height. But I expect it will take height from .frame() method.
I have a workaround for you, it's a bit messed up but works
public var body: some View {
ZStack {
ZStack(alignment: .bottom) {
Ellipse()
.fill(.yellow)
}
.frame(width: 546, height: 364)
.position(x: UIScreen.main.bounds.size.width / 2, y: Spacing.padding_0_5)
.edgesIgnoringSafeArea(.top)
.zIndex(0)
ZStack {
VStack {
VStack {
Text("Text")
.padding(.top, 42)
.foregroundColor(.red)
}
.frame(width: UIScreen.main.bounds.width, height: 182)
VStack {
Text("Your texts here")
}
.frame(width: UIScreen.main.bounds.width)
Spacer()
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
}
.zIndex(1)
}
.background(Color.red)
}
I simply made your ellipse on another layer and text on the other.
ZStack(alignment: .bottom) {
Ellipse()
.fill(.yellow)
}
.frame(width: 546, height: 364)
.position(x: UIScreen.main.bounds.size.width / 2, y: Spacing.padding_0_5)
.edgesIgnoringSafeArea(.top)
.zIndex(0)
The .zIndex(0) makes sure that the view is in the background.
ZStack {
VStack { // This VStack contains all your text
VStack { // First VStack
Text("Text")
.padding(.top, 42)
.foregroundColor(.red)
}
.frame(width: UIScreen.main.bounds.width, height: 182)
VStack { //Second VStack
Text("Your texts here")
}
.frame(width: UIScreen.main.bounds.width)
Spacer()
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
}
.zIndex(1)
Here, the ZStack takes up the entire screen. We added a VStack which contains your texts.
The first VStack has your main label over the Ellipse, and its frame is hardcoded according to the height of the Ellipse (1/2 the height as the other half of the ellipse is outside the screen).
The second VStack starts from the end of our first VStack which was the functionality needed, finally added a spacer() so that the text is placed at the top rather than middle.
The zIndex(1) makes sure that is placed over the elements at zIndex(0)
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 need to clip the view behind a Text by using its Rectangle. When I add a Text over this 1-pixel height rectangle, I need it to "clip" the subview below, so the text can be readable.
Of course, if I use a solid background color, it's easy to do, as I just set it and it will clip the subview.
Here is a POC to test it:
struct test: View {
let gradient = Gradient(colors: [Color.blue, Color.purple])
var body: some View {
ZStack {
Rectangle()
.fill(LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing))
.frame(width: 200, height: 200)
ZStack {
Rectangle()
.fill(Color.white)
.frame(width: 100, height: 1, alignment: .center)
Text("XXXX")
.background(Color.green)
}
}
}
}
Any ideas? I don't think I can handle it using a mask.
Sometimes a solution might be not-to-do instead of to-do-but.
Here is a possible implementation of above principle to solve your issue.
var body: some View {
ZStack {
Rectangle()
.fill(LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing))
.frame(width: 200, height: 200)
HStack(spacing: 0) {
Rectangle()
.fill(Color.white)
.frame(width: 30, height: 1, alignment: .center)
Text("XXXX")
Rectangle()
.fill(Color.white)
.frame(width: 30, height: 1, alignment: .center)
}
}