Tracking scroll position in a List SwiftUI - xcode

So in the past I have been tracking the scroll position using a scroll view but I've fallen into a situation where I need to track the position using a List. I am using a List because I want some of the built in real estate to create my views such as the default List styles.
I can get the value using PreferenceKeys, but the issue is when I scroll to far upwards, the PreferenceKey value will default back to its position 0, breaking my show shy header view logic.
This is the TrackableListView code
struct TrackableListView<Content: View>: View {
let offsetChanged: (CGPoint) -> Void
let content: Content
init(offsetChanged: #escaping (CGPoint) -> Void = { _ in }, #ViewBuilder content: () -> Content) {
self.offsetChanged = offsetChanged
self.content = content()
}
var body: some View {
List {
GeometryReader { geometry in
Color.clear.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("ListView")).origin)
}
.frame(width: 0, height: 0)
content
.offset(y: -10)
}
.coordinateSpace(name: "ListView")
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged)
}
}
private struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { }
}
And this is my ContentView:
struct ContentView: View {
#State private var contentOffset = CGFloat(0)
#State private var offsetPositionValue: CGFloat = 0
#State private var isShyHeaderVisible = false
var body: some View {
NavigationView {
ZStack {
TrackableListView { offset in
withAnimation {
contentOffset = offset.y
}
} content: {
Text("\(contentOffset)")
}
.overlay(
ZStack {
HStack {
Text("Total points")
.foregroundColor(.white)
.lineLimit(1)
Spacer()
Text("20,000 pts")
.foregroundColor(.white)
.padding(.leading, 50)
}
.padding(.horizontal)
.padding(.vertical, 8)
.frame(width: UIScreen.main.bounds.width)
.background(Color.green)
.offset(y: contentOffset < 50 ? 0 : -5)
.opacity(contentOffset < 50 ? 1 : 0)
.transition(.move(edge: .top))
}
.frame(maxHeight: .infinity, alignment: .top)
)
}
.navigationTitle("Hello")
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitleDisplayMode(.inline)
.frame(maxHeight: .infinity, alignment: .top)
.background(AccountBackground())
}
}
}

The issue was that Other Views in my hierarchy (probably introduced by SwiftUI) could be sending the default value, which is why you start getting zero sometimes.
What I needed was a way to determine when to use or forward the new value, versus when to ignore it.
To do this I had to make my value optional GCPoint, with a nil default value, then in your reduce method when using PreferenceKeys you have to do:
if let nextValue = nextValue() {
value = nextValue
}
Then make sure your CGPoint is an optional value.

Related

I have 2 errors Expected expression and Expected ')' in expression list as I am a beginner in swiftUI I cannot find the error

Hello I am following a swiftUI training on the udemy site to learn the basics today I am making a magazine application and I encounter an error Expected expression and the following Expected ')' in expression list being a beginner I don't know exactly how to solve it if you can tell me exactly why there is this error
I thank you
import SwiftUI
struct ContentView: View {
// MARK: - PROPERTY
#State private var isAnimating: Bool = false
#State private var imageScale: CGFloat = 1
#State private var imageOffset: CGSize = .zero
// MARK: - FUNCTION
func resetImageState() {
return withAnimation(.spring()) {
imageScale = 1
imageOffset = .zero
}
}
// MARK: - CONTENT
var body: some View {
NavigationView {
ZStack{
// MARK - PAGE IMAGE
Image("magazine-front-cover")
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(10)
.padding()
.shadow(color: .black.opacity(0.2), radius:12, x: 2, y: 2)
.opacity(isAnimating ? 1 : 0)
.offset(x: imageOffset.width, y: imageOffset.height) .scaleEffect(imageScale)
// MARK - 1 TAP Gesture
.onTapGesture(count: 2, perform: {
if imageScale == 1 {
withAnimation(.spring()) {
imageScale = 5
}
} else {
resetImageState()
}
})
// MARK - 2. DRAG GESTURE
.gesture(
DragGesture ()
.onChanged { value in
withAnimation(.linear(duration: 1)) {
imageOffset = value.translation
}
}
.onEnded { _ in
if imageScale <= 1 {
resetImageState()
}
}
)
} // ZSTACK
.navigationTitle("Pinch & Zoom")
.navigationBarTitleDisplayMode(.inline)
.onAppear(perform: {
withAnimation(.linear(duration: 1)) {
isAnimating = true
}
})
// MARK: - INFO PANEL
.overlay(
InfoPanel(scale: imageScale, offset: imageOffset)
.padding(.horizontal)
.padding(.top, -60)
, alignment: .top
)
// MARK: - CONTROLS
.overlay(
Group {
HStack {
}
.padding(.bottom, 30)
, alignment: .bottom
)
} //: NAVIGATION
.navigationViewStyle(.stack)
}
}
// MARK - PREVIEW
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPhone 13")
}
}
I don't know exactly how to fix the problem
You have a leftover Group { without closing curly brackets.
This part of your code:
// MARK: - CONTROLS
.overlay(
Group {
HStack {
}
.padding(.bottom, 30)
, alignment: .bottom
)
Should be:
// MARK: - CONTROLS
.overlay(
HStack {
}
.padding(.bottom, 30)
, alignment: .bottom
)

Using SwiftUI DragGesture to scale down and remove the whole view but lead to infinity loop

I want to implement the animation like AppStore. Drag down detail page to scale down and remove itself view.
But when I started to drag, the console keeps printing var scale is changing and app is freezes. Looks like go into infinity loop. Here is my code:
import SwiftUI
struct TestView: View {
#State var showDetail = false
var body: some View {
ZStack {
Text("First View")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.accentColor)
.onTapGesture {
withAnimation {
showDetail = true
}
}
if showDetail {
TestDetailView(showDetail: $showDetail)
}
}
.ignoresSafeArea()
}
}
struct TestDetailView: View {
#State var scale: CGFloat = 1 {
didSet {
print("scale: \(scale)")
}
}
#Binding var showDetail: Bool
var body: some View {
ScrollView {
VStack(spacing: 0) {
Text("Upper Part")
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
.gesture(DragGesture(minimumDistance: 0)
.onChanged { val in
let s = val.translation.height / UIScreen.main.bounds.height
if s > 0 && 1 - s > 0.7 {
scale = 1 - s
if scale < 0.8 {
withAnimation {
showDetail = false
}
}
}
}
)
Text("Lower Part")
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.primary)
.onTapGesture {
withAnimation(.easeInOut) {
showDetail = false
}
}
}
.frame(height: UIScreen.main.bounds.height)
}
.scaleEffect(scale)
.animation(.easeInOut, value: showDetail)
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
I know it's relative to the drag gesture and setting scale but I don't know how to fix that.
The images above is what I want to implement, my code simplified
the layout, you may try to drag down the upper part, then you'll know what happen.

swiftui freeze when scroll too fast in scrollview with lazyvgrid

I have a widget list in scrollview, each row have 3 elements. Some elements will show a stopwatch by SwiftUI Text timer. The Text timer will increase by 1 every second. Every thing work well at first, but If I scroll the list too fast, the Text timer will be freeze, won't update anymore, unless scroll the list again.
Bellow recording is my screen recording at iPhone 12 Pro Max. For the first time, slowly scroll from the "Category 2" position to the top, and the stopwatch refreshes normally. For the second and third times, quickly scroll to the top, and the stopwatch stops and never refreshes.
Here is my code:
import SwiftUI
extension Date {
func formatDate(_ format: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = format
return formatter.string(from: self)
}
func getZeroData() -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
var startDateStr = self.formatDate("yyyy-MM-dd")
startDateStr = "\(startDateStr) 00:00:00"
return dateFormatter.date(from: startDateStr) ?? Date()
}
}
struct SectionHeaderView: View {
let title: String
var body: some View {
HStack() {
Text(title)
.font(.system(size: 19.0))
.fontWeight(.bold)
Spacer()
}
.padding(.leading, 10.0)
.frame(height: 44.0)
}
}
struct CellView: View {
let date: Date
let categoryIndex: Int
let rowIndex: Int
init(date: Date, categoryIndex: Int, rowIndex: Int) {
self.date = date
self.categoryIndex = categoryIndex
self.rowIndex = rowIndex
//print("CellView init is \(self.categoryIndex)->\(self.rowIndex)")
}
var body: some View {
print("CellView show is \(self.categoryIndex)->\(self.rowIndex)")
// 1. cellview has a very complex layout, so I need to use GeometryReader to get size for inner complex component.
// 2. In order to show the problem conveniently, only the test code is shown here, but the problem in the video above can also be reproduced
return GeometryReader { geo in
VStack() {
Text("\(categoryIndex) -> \(rowIndex)")
if((self.categoryIndex==0 || self.categoryIndex==2) && self.rowIndex<=4) {
Text(date.getZeroData(), style: .timer)
Text(date.formatDate("HH:mm:ss"))
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.gray.opacity(0.2))
}
}
}
struct ContentView: View {
let date: Date = Date()
let widgetGap: CGFloat = 10.0
let widgetWidth: CGFloat
let widgetHeight: CGFloat
let columns: [GridItem]
//let columns: [GridItem] = Array(repeating: .init(.fixed(UIScreen.main.bounds.width/3.0), spacing: 0), count: 3)
init() {
let col: Int = 3
widgetWidth = (UIScreen.main.bounds.width - widgetGap * CGFloat(col + 1))/CGFloat(col)
//let widgetSize = CGSize(width: 160, height: 160)
widgetHeight = widgetWidth
columns = Array(repeating: .init(.fixed(widgetWidth), spacing: widgetGap), count: col)
}
var body: some View {
print("ContentView show")
return ScrollView() {
ForEach(0 ..< 6, id: \.self) { categoryIndex in
Section(header: SectionHeaderView(title: "Category \(categoryIndex)") ){
LazyVGrid(columns: columns, spacing: widgetGap + 5.0) {
ForEach(0 ..< 13, id: \.self) { rowIndex in
// 1. cellview has a very complex layout, so I need to use compositingGroup before cornerRadius to get a high accuracy radius
CellView(date: date, categoryIndex: categoryIndex, rowIndex: rowIndex)
.frame(width: widgetWidth, height: widgetHeight, alignment: .center)
.compositingGroup()
.cornerRadius(10)
.shadow(color: .black.opacity(0.05), radius: 6)
}
}
}
}
}
}
}
I also try to use List to replace Scrollview + Lazyvgrid. This situation has improved. But List is hard to custom style, such as remove seperateline , remove padding.
Is there any way to solve this problem?
SOS!!

Animation with offset lags behind

I want a navigation bar to stick down to the scrollview when scrolling up beyond the "regular scrollview". I use .offset() and GeometryReader for that and it's working. However, the navigation bar noticeably lags behind: Video.
Is there another approach to achieving the sticky navigation bar or something that can be changed about this one? Am I using too many views?
struct V_Home: View {
var previewData = PreviewData()
#State var size: CGRect = .zero
var body: some View {
GeometryReader { geometry in
ZStack {
ScrollView {
VStack {
// used to read the scroll position
GeometryReader { proxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: proxy.frame(in: .named("scrollView")))
}
.frame(height: 0)
.onPreferenceChange(SizePreferenceKey.self) { preferences in
self.size = preferences
}
// List
ForEach(previewData.ScoreSessionList) { scoreSession in
NavigationLink(destination: V_SessionDetail(scoreSession: scoreSession)) {
HStack(spacing: 0) {
V_ScoreSessionListItem(scoreSession: scoreSession)
}
}.padding(.top, 10)
}
.padding([.leading, .trailing], 25)
}
}
.coordinateSpace(name: "scrollView")
// NavBar
VStack {
// This Rectangle is offset to match the scroll position
// Is is lagging behind noticably
Rectangle()
.fill(Color(.green))
.frame(height: 80)
.offset(y: self.size.minY > 0 ? self.size.minY : 0)
.padding(0)
Spacer()
}
}
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
// used to make scrollview position accessible to other view
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGRect
static var defaultValue: Value = .zero
static func reduce(value _: inout Value, nextValue: () -> Value) {
_ = nextValue()
}
}

Animate the width of a Rectangle over time

In SwiftUI on WatchOS, how can I animate the width of a Rectangle (or any View for that matter) so that it starts at a certain value and over a specified time animates to a different value?
Specifically, I want to animate a Rectangle to indicate the time left to the next full minute or the next 30 seconds after a minute.
All the examples I've seen are based on Timer.scheduledTimer firing at relatively high speed and setting a #State variable, but my understanding is that especially on WatchOS this should be avoided. Is there a better way?
This is the timer/state based code I have but I feel like there should be a more efficient way:
import SwiftUI
func percentage() -> CGFloat {
1 - CGFloat(fmod(Date().timeIntervalSince1970, 30) / 30)
}
struct ContentView: View {
#State var ratio: CGFloat = percentage()
let timer = Timer.publish(every: 1 / 60, on:.main, in:.common).autoconnect()
var body: some View {
GeometryReader { geometry in
ZStack {
Rectangle()
.foregroundColor(Color.gray)
.frame(width:geometry.size.width, height:5)
HStack {
Rectangle()
.foregroundColor(Color.red)
.frame(width:geometry.size.width * self.ratio, height:5)
Spacer()
}
}
}.onReceive(self.timer) { _ in
self.ratio = percentage()
}
}
}
I think a "more efficient way" to use animation:
struct AnimationRectangle: View {
struct AnimationRectangle: View {
#State private var percentage: CGFloat = 0.0
// count, how much time left to nearest 30 seconds
#State private var animationDuration = 30 - Double(fmod(Date().timeIntervalSince1970, 30))
private var repeatedAnimationFor30Seconds: Animation {
return Animation.easeInOut(duration: 30)
.repeatForever(autoreverses: false)
}
var body: some View {
VStack {
// just showing duration of current animation
Text("\(self.animationDuration)")
ZStack {
Rectangle()
.foregroundColor(.gray)
GeometryReader { geometry in
HStack {
Rectangle()
.foregroundColor(.green)
.frame(width: geometry.size.width * self.percentage)
Spacer()
}
}
}
.frame(height: 5)
.onAppear() {
// first animation without repeating
withAnimation(Animation.easeInOut(duration: self.animationDuration)) {
self.percentage = 1.0
}
// other repeated animations
DispatchQueue.main.asyncAfter(deadline: .now() + self.animationDuration) {
self.percentage = 0.0
self.animationDuration = 30.0
withAnimation(self.repeatedAnimationFor30Seconds) {
self.percentage = 1.0
}
}
}
}
}
}
struct AnimationRectangle_Previews: PreviewProvider {
static var previews: some View {
AnimationRectangle()
}
}

Resources