swiftui freeze when scroll too fast in scrollview with lazyvgrid - animation

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!!

Related

How to optimize SwiftUI Map drag performance when many annotaitons showing under macOS?

This is really a curious problem for SwiftUI Map under macOS.
The code is definitely the same both on macOS & iOS/iPadOS, if we add tens of annotations on the map, then drag around, it's very smooth on the iOS/iPadOS, but feeling stuck, stuck and stuck on the macOS.
Is there any special coding thing we forget to setting on macOS for SwiftUI map? It's really confusion for the same code but different performance consequences...
Here is the full project files you may download from OneDrive:
https://1drv.ms/u/s!Ank9gPMc1GKYk_UlZ2V3BC_BArMofQ?e=Nsco9k
Below is the code you can check to:
import SwiftUI
import MapKit
struct ContentView: View {
#State var annotationTrackList = [TyphoonMKAnnotation]()
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 31.7, longitude: 118.4),
latitudinalMeters: 100_000,
longitudinalMeters: 100_000)
var body: some View {
ZStack {
Map(coordinateRegion: $region, annotationItems: annotationTrackList) { item in
MapAnnotation(coordinate: item.coordinate) {
ZStack(alignment: .center) {
//color
Image(systemName: "bubble.middle.bottom.fill")
.resizable()
.frame(width: 32, height: 32)
.foregroundColor(item.color)
.aspectRatio(contentMode: .fit)
//icon
Image(systemName: "tornado")
.resizable()
.frame(width: 16, height: 16)
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
}
//location title
Text(item.title ?? "")
.font(Font.system(size: 11, weight: .semibold))
.foregroundColor(.white)
}
}
}
.ignoresSafeArea()
.onAppear {
initialValues()
}
}
private func initialValues() {
var annotationList = [TyphoonMKAnnotation]()
let jsonData = TyphoonJsonData()
//forecast
let forecastJson = jsonData.forecast()
guard
let forecastList = forecastJson["forecast"].array
else { return }
for item in forecastList {
let type = item["type"].stringValue
let latitude = item["lat"].stringValue
let longitude = item["lon"].stringValue
let coordinate = CLLocationCoordinate2D(latitude: Double(latitude)!, longitude: Double(longitude)!)
let annotation = TyphoonMKAnnotation(coordinate: coordinate)
annotation.title = type
annotation.color = .pink
annotationList.append(annotation)
}
//history
let trackJson = jsonData.track()
guard
let trackList = trackJson["track"].array
else { return }
for item in trackList {
let type = item["type"].stringValue
let latitude = item["lat"].stringValue
let longitude = item["lon"].stringValue
let coordinate = CLLocationCoordinate2D(latitude: Double(latitude)!, longitude: Double(longitude)!)
let annotation = TyphoonMKAnnotation(coordinate: coordinate)
annotation.title = type
annotation.color = .green
annotationList.append(annotation)
}
self.annotationTrackList = annotationList
}
}
class TyphoonMKAnnotation: Identifiable {
let id = UUID()
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
var index: Int?
var color: Color?
init(coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
}
}

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
)

animation 'has been deprecated in iOS 15.0: use instead with Animation or animation (_: value :)

I apply ".animation (.easeIn)" but it is deprecated.
tells me: animation 'has been deprecated in iOS 15.0: use instead with Animation or animation (: value :), but animation (: value :) I don't know which value I need to pass. I can't create animations that start slowly. Can you please help me understand how I can properly use animation (_: value :) to make animation work? I have to create the animation that when I move the slider, the images slowly recompose in space according to how much they are enlarged or reduced.
thank you.
'''
import SwiftUI
struct GalleryView: View {
// MARK: - PROPERTIES
#State private var selectedAnimal: String = "lion"
let animals: [Animal] = Bundle.main.decode("animals.json")
let haptics = UIImpactFeedbackGenerator(style: .medium)
#State private var gridLayout: [GridItem] = [GridItem(.flexible())]
#State private var gridColumn: Double = 3.0
func gridSwitch() {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
}
// MARK: - BODY
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .center, spacing: 30) {
// MARK: - IMAGE
Image(selectedAnimal)
.resizable()
.scaledToFit()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 8))
// MARK: - SLIDER
Slider(value: $gridColumn, in: 2...4, step: 1) // se aumento il valore nello slider ci saranno piĆ¹ sezioni
.padding(.horizontal)
.onChange(of: gridColumn, perform: { value in
gridSwitch()
})
// MARK: - GRID
LazyVGrid(columns: gridLayout, alignment: .center, spacing: 10) {
ForEach(animals) { item in
Image(item.image)
.resizable()
.scaledToFit()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 1))
.onTapGesture {
selectedAnimal = item.image
haptics.impactOccurred()
}
} //: LOOP
} //: GRID
.animation(.easeIn)
.onAppear(perform: {
gridSwitch()
})
} //: VSTACK
.padding(.horizontal, 10)
.padding(.vertical,50)
} //: SCROLL
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(MotionAnimationView())
}
}
// MARK: - PREVIEW
struct GalleryView_Previews: PreviewProvider {
static var previews: some View {
GalleryView()
}
}
As the error says, .animation() is deprecated, with iOS 15, you need to use .animation(value:) now. where the value is the binding that you want your animation to be triggered, in your case, I assume it is selectedAnimal.
Applying this would be something like
VStack {
}
.animation(.easeIn, value: selectedAnimal)
As discussed in comments, if you want your gridLayout to be animatable, it is a little bit more tricky. Because Arrays has to be Equatable if you want them to be animatable, since extending the GridItem is not a good solution, I came up with this:
delete your .animation method change your gridSwitch function with this:
struct GalleryView: View {
// ... irrelevant code
#State private var isGridChanged = false
func gridSwitch() {
if (isGridChanged) {
withAnimation {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
}
}
else {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
isGridChanged = true
}
}
isGridChanged is required because as you're changing your gridLayout when your View is initialized, it causes a weird bug that everything is getting scaled down when app launches because of withAnimation.

Tracking scroll position in a List SwiftUI

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.

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