In my iOS app (which is using SwiftUI) I am using a scroll view in which there is vertical scrolling. My issue is: I want to scroll only from top to bottom, and not bottom to top.
Is that possible?
Thank you.
Use LazyVGrid
and DragGesture
when changed, you only allow scroll to bottom.
struct ContentView: View {
#State private var offset: CGFloat = .zero
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
var body: some View {
LazyVGrid(columns: columns) {
ForEach((0...79), id: \.self) {
let codepoint = $0 + 0x1f600
let codepointString = String(format: "%02X", codepoint)
Text("\(codepointString)")
let emoji = String(Character(UnicodeScalar(codepoint)!))
Text("\(emoji)")
}
}
.font(.largeTitle)
.offset(y: offset)
.gesture(DragGesture().onChanged {value in
let translation = value.translation.height
if translation < 1 {
offset = translation
}
})
}
}
Update
You need to track the last offset when scrolling and reset it to zero each time you stop scrolling. This fixes the issue you have addressed in your comment.
struct ContentView: View {
#State private var offset: CGFloat = .zero
#State private var lastOffset: CGFloat = .zero
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
var body: some View {
LazyVGrid(columns: columns) {
ForEach((0...79), id: \.self) {
let codepoint = $0 + 0x1f600
let codepointString = String(format: "%02X", codepoint)
Text("\(codepointString)")
let emoji = String(Character(UnicodeScalar(codepoint)!))
Text("\(emoji)")
}
}
.font(.largeTitle)
.offset(y: offset)
.gesture(DragGesture().onChanged {value in
let translation = value.translation.height
if translation <= lastOffset {
offset = translation
lastOffset = translation
}
}.onEnded { value in
lastOffset = .zero
})
}
}
You do not need to use LazyVGrid if you your Views are not too many.
Related
I have a list of Texts in a LazyVStack that is embedded within a GeometryReader like so
#State var profilePictureEnlarge: Bool = false
#State var showLiveTutorial: Bool = false
#State var textInput: String = ""
#Namespace var namespace
// State variables from LegacyPostView
#State var user: User = exampleUsers[0]
#State var boujees: [LivePost] = []
#State var addBoujee: Bool = false
#State private var listener: ListenerRegistration?
#State private var scrollProxy: ScrollViewProxy? = nil
var textContent: some View {
ScrollView {
ScrollViewReader { proxy in
LazyVStack {
ForEach(boujees, id: \.self) { boujee in
GeometryReader { geo in
let midY = geo.frame(in: .named("scrollView")).midY
ZStack {
let height = (boujee.text.count / 40)
Text("**\(boujee.anonymous ? "anon".uppercased() : boujee.authorUsername.lowercased()):** \(boujee.text)")
.font(.title2)
.fontWeight(.heavy)
.blur(radius: 30 * (1 - (midY / 130)))
}
.padding(.horizontal, 20)
.opacity(midY / 130 > 1 ? 1 : midY / 130)
}
.frame(maxWidth: .infinity, minHeight: 150, maxHeight: 400)
}
}
.onAppear {
scrollProxy = proxy
// In the beginning scroll to bottom without animation.
scrollProxy?.scrollTo(boujees.last?.id)
}
.onTapGesture {
if profilePictureEnlarge {
withAnimation(.spring()) {
profilePictureEnlarge.toggle()
}
}
hideKeyboardOnTap()
}
}
}
.safeAreaInset(edge: .bottom, content: {
inputField
.offset(x: 0, y: profilePictureEnlarge ? 150 : 0)
})
.coordinateSpace(name: "scrollView")
}
I need the geometry reader to blur the text as it disappears out of the screen during scrolling. This messes up the height of each Text view like so
As you can see, there's unnecessary spacing in between text views that aren't as long as other views. SwiftUI handles this automatically but the GeometryReader messes up this auto-adjustment. This is because I set a minHeight value for the GeometryReader frame. I'd like to have text views adjust accoring to the line count, but I'm not sure how.
Here's a main ContentView and DetailedCardView and when open DetailedCardView there's a ScrollView with more content. It also have closeGesture which allow to close the card when swiping from left to right and vice versa. So, at that point problems start to appear, like this ones inside DetailedCardView:
When scrolling in ScrollView then safeAreaInsets .top and .bottom start to be visible and as a result it's shrink the whole view vertically.
The closeGesture and standard ScrollView gesture while combined is not smooth and I'd like to add closing card not only when swiping horizontally but vertically too.
As a result I'd like to have the same gesture behaviour like opening and closing detailed card in App Store App Today section. Would love to get your help 🙌
ContentView DetailedCardView closeGesture in Action safeAreaInsets
struct ContentView: View {
#State private var showCardView = false
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
#State private var selectedCardNumber = 0
var body: some View {
ZStack {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(1..<101) { number in
colors[number % colors.count]
.overlay(Text("\(number)").font(.largeTitle.bold()).foregroundColor(.white))
.frame(height: 300)
.cornerRadius(30)
.padding(10)
.onTapGesture {
showCardView.toggle()
selectedCardNumber = number
}
}
}
}
.opacity(showCardView ? 0 : 1)
.animation(.spring(), value: showCardView)
if showCardView {
CardDetailView(showCardView: $showCardView, number: selectedCardNumber)
}
}
}
}
struct CardDetailView: View {
#Binding var showCardView: Bool
#State private var cardPosition: CGSize = .zero
let number: Int
var body: some View {
ScrollView {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(.green)
.overlay(Text("\(number)").font(.system(size: 100).bold()).foregroundColor(.white))
.frame(height: 500)
Color.red.opacity(0.8)
.frame(height: 200)
Color.gray.opacity(0.8)
.frame(height: 200)
Color.blue
.frame(height: 200)
Color.yellow
.frame(height: 200)
}
.scaleEffect(gestureScale())
.gesture(closeGesture)
}
private var closeGesture: some Gesture {
DragGesture()
.onChanged { progress in
withAnimation(.spring()) {
cardPosition = progress.translation
}
}
.onEnded { _ in
withAnimation(.spring()) {
let positionInTwoDirections = abs(cardPosition.width)
if positionInTwoDirections > 100 {
showCardView = false
}
cardPosition = .zero
}
}
}
private func gestureScale() -> CGFloat {
let max = UIScreen.main.bounds.width / 2
let currentAmount = abs(cardPosition.width)
let percentage = currentAmount / max
return 1 - min(percentage, 0.5)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm attempting to drag a view (the ZStack below) using the dragPiece gesture in the following code. If I have the .onChanged modifier commented out, this code works fine, in the sense that the view ends up repositioned. But when .onChanged is uncommented and active, the drag gesture seems to get stuck, repeatedly printing out the same width and height values, and the .onEnded modifier is never executed. This seems like it should be straightforward, so clearly I'm missing something. Any help will be appreciated.
struct PieceView: View {
var id: String
#State private var orientation: Int
#State private var dragOffset = CGSize.zero
#State var selected = false
var dragPiece: some Gesture {
DragGesture()
.onChanged { value in
dragOffset = value.translation
print("width: \(dragOffset.width), height: \(dragOffset.height)")
}
.onEnded{ value in
print("\(id) dragged")
dragOffset = value.translation
print("width: \(dragOffset.width), height: \(dragOffset.height)")
}
}
var body: some View {
ZStack {
Image(id + "\(orientation)")
Image("boardSquare")
.padding(0)
.gesture(dragPiece)
}
.offset(dragOffset)
}
init() {
id = ""
orientation = 0
}
init(id: String, orientation: Int = 0, gesture: String = "") {
self.id = id
self.orientation = orientation
}
}
It appears that .offset(dragOffset) must appear before .gesture(dragPiece). The following code works as expected:
struct PieceView: View {
var id: String
#State private var orientation: Int
#State private var dragOffset = CGSize.zero
#State var selected = false
var dragPiece: some Gesture {
DragGesture()
.onChanged { value in
dragOffset = value.translation
print("width: \(dragOffset.width), height: \(dragOffset.height)")
}
.onEnded{ value in
print("\(id) dragged")
dragOffset = value.translation
print("width: \(dragOffset.width), height: \(dragOffset.height)")
}
}
var body: some View {
ZStack {
Image(id + "\(orientation)")
Image("boardSquare")
.padding(0)
.offset(dragOffset)
.gesture(dragPiece)
}
}
init() {
id = ""
orientation = 0
}
init(id: String, orientation: Int = 0, gesture: String = "") {
self.id = id
self.orientation = orientation
}
}
In my case, this leaves me with some other UI design issues, because I want the gesture to apply to Image("boardSquare") but the whole ZStack to be dragged. But that's a separate issue, and at least now I know what the problem was with my current code.
Hello I'm looking to replicate a swiftui animation that I see around specifically to animate a view (like a button) from where it is in the hierarchy to full screen and then back.
Here is a demo
https://imgur.com/a/qG5iaAB
What I've tried
I don't have a lot to go on with this. I basically tried manipulating frame dimensions with gesture inputs, but this breaks down pretty quickly once you have anything else on the screen.
the below works with a single element and nothing else 🤷🏻♀️.
struct Expand: View {
#State private var dimy = CGFloat(100)
#State private var dimx = CGFloat(100)
#State private var zz = 0.0
#State private var offset = CGFloat(0)
let w = UIScreen.main.bounds.size.width
let h = UIScreen.main.bounds.size.height
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
Image(systemName: "play")
.foregroundColor(Color.red)
.frame(width: dimx, height: dimy)
.background(Color.red.opacity(0.5))
.border(Color.red)
.offset(y: offset)
.gesture(DragGesture()
.onChanged { gesture in
let y = gesture.translation.height
offset = y
}
.onEnded { _ in
offset = 0
dimx = dimx == 100 ? UIScreen.main.bounds.size.width : 100
dimy = dimy == 100 ? UIScreen.main.bounds.size.height : 100
zz = zz == 0 ? 3 : 0
}
)
.animation(.default)
.position(x: w / 2, y: h / 2)
.zIndex(zz)
}
}
.edgesIgnoringSafeArea(.vertical)
}
}
I want to create a custom scroll that snaps views to middle
But I can't figure out how to set the offset correctly.
This is the sample of the scroll:
struct contentView: View {
#State var startOffset: CGFloat = 0
#State var translationWidth: CGFloat = 0
#State var offset: CGFloat = 0
#State var index: Int = 0
var body: some View {
GeometryReader { geo in
HStack {
ForEach(0..<5, id: \.self) { num in
Button(action: {
// some action
}) {
Circle()
}
.frame(width: geo.size.width)
.offset(x: -self.offset*CGFloat(self.index) + self.translationWidth)
}
}.frame(width: geo.size.width, height: 200)
.offset(x: self.startOffset )
.highPriorityGesture (
DragGesture(minimumDistance: 0.1, coordinateSpace: .global)
.onChanged({ (value) in
self.translationWidth = value.translation.width
})
.onEnded({ (value) in
if self.translationWidth > 40 {
self.index -= 1
} else if self.translationWidth < -40 {
self.index += 1
}
self.translationWidth = 0
})
)
.onAppear {
withAnimation(.none) {
let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
self.offset = geo.size.width
self.startOffset = itemsWidth/2
}
}
}
}
}
It works but it is slightly off the middle, it doesn't scroll exactly in the middle and I don't understand why.
The problem occurs with the onAppear closure:
.onAppear {
withAnimation(.none) {
let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
self.offset = geo.size.width
self.startOffset = itemsWidth/2
}
}
I might be missing some small pixels in my calculations.
UPDATE
So Apparently the HStack has a default value for spacing between views.
so to fix it you should remove it like so:
HStack(spacing: 0) {
....
}
So after a bit of experimenting, I realized the HStack is using a default spacing which is not equal to zero.
It means that in my calculations I didn't include the spacing between items
so to fix this all you need to add is:
HStack(spacing: 0) {
...
}