Search code examples
iosswiftflutteruikitview

How to destroy timer when navigate to other screen


Using UiKitView to show swift UI code in flutter app.

import SwiftUI

struct Snowflake: Identifiable {
    var id = UUID()
    var x: Double
    var y: Double
    var scale: Double
    var speed: Double
}


struct SnowFallAnimationView: View{
    @State private var snowflakes: [Snowflake] = []
    @State private var timer: Timer?
    
    var body: some View {
        ZStack {
            LinearGradient(
                gradient: Gradient(colors: [
                    .black,
                    .black
                ]), startPoint: .top, endPoint: UnitPoint.bottom
            )
            //.ignore safe area
            
            if #available(iOS 15.0, *) {
                Canvas { context, size in
                    
                    for snowflake in snowflakes {
                        context.draw(Text("❄️").font(.system(size: 10 * snowflake.scale)), at: CGPoint(x: snowflake.x * size.width, y: snowflake.y * size.height))
                    }
                }
            } else {
                // Fallback on earlier versions
            }
        }.onAppear() {
            startSnowFall()
        }
    }
    func startSnowFall() {
        for _ in 0..<50 {
            snowflakes.append(
                Snowflake(
                    x: Double.random(in: 0...1),
                    y: Double.random(in: -0.2...0),
                    scale: Double.random(in: 0.5...1.5),
                    speed: Double.random(in: 0.001...0.003)
                )
            )
        }
        timer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true, block:     {
            _ in
            for i in snowflakes.indices {
                snowflakes[i].y += snowflakes[i].speed
                if snowflakes[i].y >= 1.2 {
                    snowflakes[i].y = -0.2
                    snowflakes[i].x = Double.random(in: 0...1)
                }
            }
        })
    }
}

Flutter side.

class NativeViewExample extends StatefulWidget {
  const NativeViewExample({super.key});

  @override
  State<NativeViewExample> createState() => _NativeViewExampleState();
}

class _NativeViewExampleState extends State<NativeViewExample> {
  @override
  Widget build(BuildContext context) {
    const String viewType = '<platform-view-type>';
    // Pass parameters to the platform side.
    final Map<String, dynamic> creationParams = <String, dynamic>{};
    return Stack(
      children: [
        UiKitView(
          viewType: viewType,
          layoutDirection: TextDirection.ltr,
          creationParams: creationParams,
          creationParamsCodec: const StandardMessageCodec(),
        ),
        Align(
          alignment: Alignment.bottomCenter,
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: ElevatedButton(
              onPressed: () {
                // Flutter button functionality
                print("Flutter button pressed");
              },
              child: Text('Flutter Button'),
            ),
          ),
        ),
      ],
    );
  }
}

The problem is: Timer is being created every time the SnowFallAnimationView appears on screen. This results in multiple timers running simultaneously, which leads to the snowflakes accelerating over time. (It happens when navigating to another screen and coming back).

Is there's any way to stop the timer (in swift code) when navigating to another screen. Or, Just work with already created timer. (prevent creation of multiple timers).


Solution

  • .appear may be called more than once within the View life cycle. So, you should create the timer a single time to prevent duplication and invalidate the timer on .disappear, something like:

    var body: some View {
        ...
        .onAppear {
            startSnowFall()
        }
        .onDisappear {
            stopSnowFall()
        }
    }
    
    func startSnowFall() {
        guard timer == nil else { return }
        ...
    }
    
    func stopSnowFall() {
        guard timer != nil else { return }
        timer?.invalidate()
        timer = nil
        snowflakes.removeAll()
    }