Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple PopoverTip Modifiers in SwiftUI: Persistent Display Glitch

I've encountered an issue when attempting to add multiple popoverTip modifiers in my SwiftUI code. Regardless of whether there's a specified rule or parameter, the tips begin to constantly appear and disappear. Is this a recognized issue? How can we sequentially display multiple tip popovers on complex views? Even when one tip is invalidated, the glitch persists. Should this be used only for views without any state updates?

Here's a sample code that demonstrates the problem:

import SwiftUI
import TipKit

@main
struct testbedApp: App {
    var body: some Scene {
        WindowGroup {
          ContentView()
        }
    }
  
  init() {
    try? Tips.configure()
  }
}

struct PopoverTip1: Tip {
    var title: Text {
        Text("Test title 1").foregroundStyle(.indigo)
    }

    var message: Text? {
        Text("Test message 1")
    }
}

struct PopoverTip2: Tip {
    var title: Text {
        Text("Test title 2").foregroundStyle(.indigo)
    }

    var message: Text? {
        Text("Test message 2")
    }
}

struct ContentView: View {
    private let timer = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
  
    @State private var counter = 1
    
    var body: some View {
        VStack(spacing: 20) {
            Spacer()
            Text("Counter value: \(counter)").popoverTip(PopoverTip1())
            Spacer()
            Text("Counter value multiplied by 2: \(counter * 2)")
                .foregroundStyle(.tertiary)
                .popoverTip(PopoverTip2())
            Spacer()
        }
        .padding()
        .onReceive(timer) { _ in
          counter += 1
        }
    }
}

#Preview {
    ContentView()
}
like image 967
Arietis Avatar asked Oct 26 '25 06:10

Arietis


1 Answers

Should this be used only for views without any state updates?

It would seem so. You can certainly get it to work by avoiding updates in the view that is showing the tips.

It works with the following changes:

  • Move the counter to an ObservableObject that is created (but not observed) in the parent view.
  • Factor-out the two Text views to separate views and pass them the wrapped counter to observe.

With just these changes to your original code, it works to the extent that tip 1 is shown every time you restart the app, but tip 2 is never shown. In order that tip 1 is recorded as seen, you need to add a tap gesture that invalidates the tip, as explained in the documentation. Then, the next time you start the app, tip 2 is shown.

Here is your example with all the changes applied:


class PublishedCounter: ObservableObject {
    @Published private var val = 1

    var value: Int {
        val
    }

    func increment() {
        val += 1
    }
}

struct Text1: View {
    @ObservedObject var counter: PublishedCounter

    var body: some View {
        Text("Counter value: \(counter.value)")
    }
}

struct Text2: View {
    @ObservedObject var counter: PublishedCounter

    var body: some View {
        Text("Counter value multiplied by 2: \(counter.value * 2)")
            .foregroundStyle(.tertiary)
    }
}

struct ContentView: View {
    private let counter = PublishedCounter()
    private let timer = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
    private var tip1 = PopoverTip1()
    private var tip2 = PopoverTip2()

    var body: some View {
        VStack(spacing: 20) {
            Spacer()
            Text1(counter: counter)
                .popoverTip(tip1)
                .onTapGesture {

                    // Invalidate the tip when someone uses the feature
                    tip1.invalidate(reason: .actionPerformed)
                }
            Spacer()
            Text2(counter: counter)
                .popoverTip(tip2)
                .onTapGesture {

                    // Invalidate the tip when someone uses the feature
                    tip2.invalidate(reason: .actionPerformed)
                }
            Spacer()
        }
        .padding()
        .onReceive(timer) { _ in
            counter.increment()
        }
    }
}

To reset all tips (for testing purposes), add a line to purge the tips to the init function of the app, before Tips.configure():

init() {

    // Purge all TipKit-related data and reset the state of all tips
    try? Tips.resetDatastore()

    try? Tips.configure()
}
like image 73
Benzy Neez Avatar answered Oct 29 '25 06:10

Benzy Neez



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!