skip to Main Content

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()
}

2

Answers


  1. 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()
    }
    
    Login or Signup to reply.
  2. It is a bit hacky and fragile but you can use a shared hardcoded Event Rule to present sequential tips.

    Each Tip would have a hardcoded count.

    struct PopoverTip1: Tip {
    
        var title: Text {
            Text("Test title 1").foregroundStyle(.indigo)
        }
        var rules: [Rule] {
            #Rule(TipSampleView.tipDidOpen) {
                $0.donations.count == 1
            }
        }
        var message: Text? {
            Text("Test message 1")
        }
    }
    

    Then onReceive send a donation.

    import SwiftUI
    import TipKit
    struct TipSampleView: View {
        static let tipDidOpen = Tips.Event(id: "tipDidOpen")
        @State private var timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
        
        var body: some View {
            VStack {
                Text("Tip 1")
                    .popoverTip(PopoverTip1())
                
                Text("Tip 2")
                    .popoverTip(PopoverTip2())
            }
            .task {
                do {
                    try Tips.configure([.displayFrequency(.immediate)])
                } catch {
                    print(error)
                }
            }
            .onReceive(timer) { timer in
                TipSampleView.tipDidOpen.sendDonation()
                
                if TipSampleView.tipDidOpen.donations.count >= 4{
                    try? Tips.resetDatastore()
                }
            }
        }
    }
    

    It is a bit tacky because from my testing the hardcoded count has to be "odd"

    struct PopoverTip2: Tip {
        static let id: Int = 2
        var title: Text {
            Text("Test title 2").foregroundStyle(.indigo)
        }
        var rules: [Rule] {
            #Rule(TipSampleView.tipDidOpen) {
                $0.donations.count == 3 //Odd
            }
        }
    
        var message: Text? {
            Text("Test message 2")
        }
    }
    

    If they are sequential the "even" get skipped.

    I used the documentation for the Tips.Event screen on the Apple website.

    https://developer.apple.com/documentation/tipkit/tips/event

    The logic behind this solution is that only one tip is eligible to be shown at a time.

    It seems like tip kit evaluates what tips to show at every reload of the body and presents/dismisses anything that is eligible. I think this is intentional but a bug report wouldn’t hurt.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search