0

I have 4 tips to show in screen so i have created like this

code:

struct TipContent {
 let title: String
 let message: String
}
// MARK: - TipKit Manager
class TipKitManager: ObservableObject {
 static let shared = TipKitManager()
 
 // All tip contents in a single array
 let tipContents: [TipContent] = [
 TipContent(title: "Welcome!", message: "Start your journey by logging in to your account."),
 TipContent(title: "Enter Username!", message: "Provide your username or email to proceed."),
 TipContent(title: "Enter Password!", message: "Use your secure password to log in safely."),
 TipContent(title: "Register!", message: "Don't have an account? Tap Register to create one!")
 ]
 
 @Published var currentTipIndex: Int = 0
 @Published var showTip: Bool = true
 
 var currentTip: LoginTip? {
 guard showTip && currentTipIndex < tipContents.count else { return nil }
 return LoginTip(index: currentTipIndex, content: tipContents[currentTipIndex])
 }
 
 var totalSteps: Int {
 tipContents.count
 }
 
 var currentStep: Int {
 currentTipIndex + 1
 }
 
 func moveToNextTip() {
 if currentTipIndex < tipContents.count - 1 {
 currentTip?.invalidate(reason: .tipClosed)
 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
 self.currentTipIndex += 1
 }
 } else {
 dismissTips()
 }
 }
 
 func dismissTips() {
 currentTip?.invalidate(reason: .tipClosed)
 showTip = false
 }
 
 func resetTips() {
 currentTipIndex = 0
 showTip = true
 }
}
// MARK: - Dynamic Tip
struct LoginTip: Tip {
 let index: Int
 let content: TipContent
 
 var title: Text {
 Text(content.title)
 }
 
 var message: Text? {
 Text(content.message)
 }
}
// MARK: - Custom Tip View Style (Refactored)
struct NewFeatureTipViewStyle: TipViewStyle {
 let step: Int
 let totalSteps: Int
 var onNext: (() -> Void)? = nil
 
 func makeBody(configuration: Configuration) -> some View {
 VStack(alignment: .leading, spacing: 10) {
 configuration.title
 .font(.title2)
 .bold()
 
 configuration.message
 .foregroundStyle(.secondary)
 
 HStack {
 // Progress dots
 HStack {
 ForEach(1...totalSteps, id: \.self) { i in
 Capsule()
 .fill(i == step ? .green : .gray.opacity(0.4))
 .frame(width: 24, height: 6)
 }
 }
 
 Spacer()
 
 if step < totalSteps {
 Button("Next") { onNext?() }
 .foregroundStyle(.green)
 } else {
 Button("Done") {
 configuration.tip.invalidate(reason: .tipClosed)
 }
 .foregroundStyle(.green)
 }
 }
 .padding(.top, 10)
 }
 .padding(.vertical, 20)
 .padding(.horizontal, 24)
 }
}

Login screen: here i am showing tips but initially 1st tipview is showing but here if i click next then it suppose to move to 2nd place(email textfield) but it is closed.. not going to 2nd and all 4... where am i wrong.. how to fix

struct LoginView: View {
 
 @StateObject private var tipManager = TipKitManager.shared
 
 var body: some View {
 
 VStack(spacing: 30) {
 
 // MARK: LOGIN HEADER
 Text("Login")
 
 .popoverTip(tipManager.currentTipIndex == 0 ? tipManager.currentTip : nil, arrowEdge: .top)
 .tipViewStyle(
 NewFeatureTipViewStyle(
 step: tipManager.currentStep,
 totalSteps: tipManager.totalSteps,
 onNext: { tipManager.moveToNextTip() }
 )
 )
 
 VStack(spacing: 20) {
 // MARK: USERNAME
 TextField("Enter email or username", text: $userName)
 .textFieldStyle(.roundedBorder)
 .popoverTip(tipManager.currentTipIndex == 1 ? tipManager.currentTip : nil, arrowEdge: .bottom)
 .tipViewStyle(
 NewFeatureTipViewStyle(
 step: tipManager.currentStep,
 totalSteps: tipManager.totalSteps,
 onNext: { tipManager.moveToNextTip() }
 )
 )
 
 // MARK: PASSWORD
 SecureField("Password", text: $uaerPassword)
 .textFieldStyle(.roundedBorder)
 .popoverTip(tipManager.currentTipIndex == 2 ? tipManager.currentTip : nil, arrowEdge: .bottom)
 .tipViewStyle(
 NewFeatureTipViewStyle(
 step: tipManager.currentStep,
 totalSteps: tipManager.totalSteps,
 onNext: { tipManager.moveToNextTip() }
 )
 )
 
 // MARK: REGISTER
 HStack {
 Text("Don't have an account?")
 .foregroundColor(.gray)
 
 Text("Register here!")
 
 .popoverTip(tipManager.currentTipIndex == 3 ? tipManager.currentTip : nil, arrowEdge: .top)
 .tipViewStyle(
 NewFeatureTipViewStyle(
 step: tipManager.currentStep,
 totalSteps: tipManager.totalSteps,
 onNext: { tipManager.moveToNextTip() }
 )
 )
 .onTapGesture {
 isRegTapped = true
 }
 }
 }
 .padding()
 
 Spacer()
 }
 
 }
 
}
Benzy Neez
29k3 gold badges22 silver badges63 bronze badges
asked Oct 27 at 11:33

2 Answers 2

1

You seem to be re-inventing the function of a TipGroup. I would suggest, it would be simpler to use this built-in functionality, then you don't need the TipKitManager.

When I tried it, I found that I could only get it working if the tips were all of a different type. This is also how it is being done in the example in the documentation. This means, instead of using LoginTip for all four cases, four different types of Tip are needed.

You could consider using an enum to encapsulate the content for the various tips:

enum TipContent {
 case welcome
 case username
 case password
 case register
 var title: String {
 switch self {
 case .welcome: "Welcome!"
 case .username: "Enter Username!"
 case .password: "Enter Password!"
 case .register: "Register!"
 }
 }
 var message: String {
 switch self {
 case .welcome: "Start your journey by logging in to your account."
 case .username: "Provide your username or email to proceed."
 case .password: "Use your secure password to log in safely."
 case .register: "Don't have an account? Tap Register to create one!"
 }
 }
}

The tips themselves can then be defined as follows:

struct WelcomeTip: Tip {
 let content = TipContent.welcome
 var title: Text { Text(content.title) }
 var message: Text? { Text(content.message) }
}
struct UsernameTip: Tip {
 let content = TipContent.username
 var title: Text { Text(content.title) }
 var message: Text? { Text(content.message) }
}
struct PasswordTip: Tip {
 let content = TipContent.password
 var title: Text { Text(content.title) }
 var message: Text? { Text(content.message) }
}
struct RegisterTip: Tip {
 let content = TipContent.register
 var title: Text { Text(content.title) }
 var message: Text? { Text(content.message) }
}

NewFeatureTipViewStyle no longer needs a callback for proceeding to the next tip, it can just invalidate the tip being shown:

struct NewFeatureTipViewStyle: TipViewStyle {
 let step: Int
 let totalSteps: Int
 func makeBody(configuration: Configuration) -> some View {
 VStack(alignment: .leading, spacing: 10) {
 configuration.title
 .font(.title2)
 .bold()
 configuration.message
 .foregroundStyle(.secondary)
 .fixedSize(horizontal: false, vertical: true)
 HStack {
 // Progress dots
 HStack {
 ForEach(1...totalSteps, id: \.self) { i in
 Capsule()
 .fill(i == step ? .green : .gray.opacity(0.4))
 .frame(width: 24, height: 6)
 }
 }
 Spacer()
 Button(step < totalSteps ? "Next" : "Done") {
 configuration.tip.invalidate(reason: .tipClosed)
 }
 .foregroundStyle(.green)
 }
 .padding(.top, 10)
 }
 .padding(.vertical, 20)
 .padding(.horizontal, 24)
 }
}

Here is how LoginView can put the tip group into operation:

struct LoginView: View {
 @State private var userName = ""
 @State private var userPassword = ""
 @State private var isRegTapped = false
 let nTips = 4
 @State private var loginTips = TipGroup(.ordered) {
 WelcomeTip()
 UsernameTip()
 PasswordTip()
 RegisterTip()
 }
 var body: some View {
 VStack(spacing: 30) {
 // MARK: LOGIN HEADER
 Text("Login")
 .popoverTip(loginTips.currentTip as? WelcomeTip, arrowEdge: .top)
 .tipViewStyle(NewFeatureTipViewStyle(step: 1, totalSteps: nTips))
 VStack(spacing: 20) {
 // MARK: USERNAME
 TextField("Enter email or username", text: $userName)
 .textFieldStyle(.roundedBorder)
 .popoverTip(loginTips.currentTip as? UsernameTip)
 .tipViewStyle(NewFeatureTipViewStyle(step: 2, totalSteps: nTips))
 // MARK: PASSWORD
 SecureField("Password", text: $userPassword)
 .textFieldStyle(.roundedBorder)
 .popoverTip(loginTips.currentTip as? PasswordTip)
 .tipViewStyle(NewFeatureTipViewStyle(step: 3, totalSteps: nTips))
 // MARK: REGISTER
 HStack {
 Text("Don't have an account?")
 .foregroundColor(.gray)
 Text("Register here!")
 .popoverTip(loginTips.currentTip as? RegisterTip, arrowEdge: .bottom)
 .tipViewStyle(NewFeatureTipViewStyle(step: 4, totalSteps: nTips))
 .onTapGesture {
 isRegTapped = true
 }
 }
 }
 .padding()
 Spacer()
 }
 }
}

Animation

answered Oct 27 at 15:27
Sign up to request clarification or add additional context in comments.

2 Comments

see your code fully cleaned version and working perfect in iOS 18.. but the problem is TipGroup supports iOS 18 +... actually my project's minimum version is iOS 16 i am already struggling with it to use.. now tipgroup is 18 +
I see, shame you didn't mention that before. TipKit requires iOS 17, so I think you're going to have difficulty getting any kind of Tip working with iOS 16. An option would be to show tips if iOS 17 is available, otherwise don't show any tips at all. You could choose to make the threshold iOS 18 instead, then TipGroup can also be used.
1

Rather than "fixing" the error, how about we just take it back to the drawing board?

I know certain influencers have made a lot people believe that class's are needed everywhere, but they're really not. They're slower and more prone to bugs. They're good for what they're good for, but everything here can be reliably localized, and I don't believe one is needed here.

So without a reference type class, lets make a value type enum that conforms to Tip to store the data...

enum LoginTipOption: Int , CaseIterable , Tip {
 
 case welcome
 case username
 case password
 case register
 
 var id : Int { self.rawValue }
 
 var title : Text {
 switch self {
 case .welcome : Text ( "Welcome!" )
 case .username : Text ( "Enter Username!" )
 case .password : Text ( "Enter Password!" )
 case .register : Text ( "Register!" )
 }
 }
 var message: Text? {
 switch self {
 case .welcome : Text ( "Start your journey by logging in to your account." )
 case .username : Text ( "Provide your username or email to proceed." )
 case .password : Text ( "Use your secure password to log in safely." )
 case .register : Text ( "Don't have an account? Tap Register to create one!" )
 }
 }
 /// Returns the next case, if one exists.
 /// Returns `nil` is `self` is the last case.
 /// Used by `NewFeatureTipViewStyle` button.
 func nextTip() -> LoginTipOption? { .init ( rawValue: self.rawValue + 1 ) }
 
 /// Returns `true` if the `self` is the last case.
 /// Used locally by `buttonString` variable.
 var isLastStep: Bool { self == Self.allCases.last }
 
 /// Used by `NewFeatureTipViewStyle` button.
 var buttonString: String { self.isLastStep ? "Done" : "Next" }
}

Notice it also conforms to CaseIterable and has a RawValue that is an Int.
These two make for an easy iteration. The RawValue also made my nextTip() -> LoginTipOption? function super simple.

And now that that is done, I can set an @State var tip: LoginTipOption? = .welcome in your LoginView.

@available( iOS 18.0 , * )
struct LoginView: View {
 init ( ) {
 try? Tips.resetDatastore()
 try? Tips.configure ( [ .displayFrequency ( .immediate ) ] )
 }
 
 @State var tip: LoginTipOption? = .welcome
 
 var body: some View {
 VStack ( spacing: 30 ) {
 Text ( "Login" )
 .tip ( .welcome , currentTip: self.tip , arrowEdge: .top )
 VStack ( spacing: 20 ) {
 TextField ( "Enter email or username" , text: .constant ( "UserName" ) )
 .tip ( .username , currentTip: self.tip , arrowEdge: .top )
 SecureField ( "" , text: .constant ( "Hidden" ) )
 .tip ( .password , currentTip: self.tip , arrowEdge: .top )
 HStack {
 Text ( "Don't have an account?" ) .foregroundColor ( .gray )
 Text ( "Register here!" )
 .tip ( .register , currentTip: self.tip , arrowEdge: .bottom )
 }
 }
 .padding()
 Spacer()
 }
 .textFieldStyle ( .roundedBorder )
 .tipViewStyle ( NewFeatureTipViewStyle ( tip: $tip ) )
 }
}

I created an extension that delivers backwards compatibility to iOS 18 for .popoverTip(...). For further backwards compatibility, I personally would just create my own TipKit library.

@available( iOS 18.0 , * )
extension View {
 @ViewBuilder
 func tip ( _ tip: LoginTipOption , currentTip: LoginTipOption? , arrowEdge: Edge? = nil ) -> some View {
 if #available( iOS 26.0 , * ) {
 // MARK: iOS 26
 popoverTip ( tip == currentTip ? tip : nil , arrowEdge: arrowEdge )
 } else if let edge = arrowEdge {
 // MARK: iOS 18
 popoverTip ( tip == currentTip ? tip : nil , arrowEdge: edge )
 .presentationCompactAdaptation ( .popover )
 } else {
 // MARK: iOS 18
 popoverTip ( tip == currentTip ? tip : nil )
 .presentationCompactAdaptation ( .popover )
 }
 }
}

And finally, I set your TipViewStyle to run off a Binding<LoginTipOption?>. Most everything is the same except the button updates the @State LoginTipOption located on the LoginView through the Binding<LoginTipOption?>.

@available( iOS 17.0 , * )
struct NewFeatureTipViewStyle: TipViewStyle {
 
 @Binding var tip: LoginTipOption?
 
 func makeBody ( configuration: Configuration ) -> some View {
 VStack ( alignment: .leading , spacing: 10 ) {
 configuration.title .font ( .title2 ) .bold()
 
 configuration.message .foregroundStyle ( .secondary )
 
 HStack {
 HStack {
 ForEach ( LoginTipOption.allCases.indices , id: \.self ) { i in
 Capsule()
 .fill ( i == self.tip?.rawValue ? .green : .gray.opacity ( 0.4 ) )
 .frame ( width: 24 , height: 6 )
 }
 }
 Spacer()
 Button ( tip?.buttonString ?? "Never" , action: { self.tip = tip?.nextTip() } )
 .foregroundStyle ( .green )
 }
 .padding ( .top , 10 )
 }
 .padding ( .vertical , 20 )
 .padding ( .horizontal , 24 )
 }
}

On the last Tip , the nextTip() -> LoginTipOption? function returns nil and the cycle is over.

answered Oct 27 at 16:05

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.