Create custom interactive tooltip in swiftUI

52 views Asked by At

I'm trying to create a custom interactive tooltip view for SwiftUI that should support from iOS 14. The goal is to create a reusable view, preferably via modifier which will present the tooltip on the selected view. The tool tip can have text and actionable buttons, and when it appears only the selected view and tooltip should be highlighted while the rest of the screen is covered by a transparent dark background. Below is the screenshot of what I want to achieve.

enter image description here enter image description here

Below is what I've been able to achieve.

enter image description here

Is this possible to do this via ViewModifier? The challenge here is to fill the background to whole main view from the inner sub view and position the tooltip based on the inner view position. Below is my code. Any help is appreciated

import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            VStack {
                ScrollView {
                    HeaderView()
                    Spacer().frame(height: 200)
                    BodyView()
                }
                
            }
        }
        .edgesIgnoringSafeArea(.all)
    }
}

struct HeaderView: View {
    var body: some View {
        ZStack {
            
            VStack {
                HStack {
                    Text("Heading")
                        .foregroundColor(.white)
                        .frame(height: 100)
                    Spacer()
                    Image(systemName: "globe")
                        .foregroundColor(.white)
                }
            }
            .padding()
            .background(Color.blue)
        }
    }
}

struct BodyView: View {
    var body: some View {
        VStack(spacing: 70){
            GreetingView()
            GreetingSubView()
        }
    }
}

struct GreetingView: View {
    var body: some View {
            VStack {
                Text("Start Step 1")
                    .padding()
                    .background(Color.green)
                    .cornerRadius(4)
                    .padding(8)
                    .toolTip(customToolTipView: createToolTipContentView(), x: 0, y: 0) {
                        print("Skip Action")
                    } secondaryAction: {
                        print("Next Action")
                    }
            }
    }
    
    func createToolTipContentView() -> AnyView {
        return AnyView(
            ToolTipView(title: "Title",
                                  subtitle: "Subtitle",
                                  primaryButtonTitle: "Skip",
                                  secondaryButtonTitle: "Next",
                                  primaryAction: {},
                                  secondaryAction: {}, position: .bottom)
        )
    }
}

struct GreetingSubView: View {
    @State private var showTooltip = true
    var body: some View {
        VStack {
            HStack {
                Text("Start Step 2")
                    .padding()
                    .background(Color.yellow)
                    .cornerRadius(4)
            }
        }
    }
}


struct ToolTipView: View {
    
    enum ToolTipPosition {
        case top
        case bottom
    }
    let title: String
    let subtitle: String
    let primaryButtonTitle: String
    let secondaryButtonTitle: String
    let primaryAction: () -> Void
    let secondaryAction: () -> Void
    let position: ToolTipPosition
    
    var body: some View {
        VStack(spacing: 0) {
            if position == .top {
                Triangle()
                    .fill(Color.white)
                    .frame(width: 20, height: 20)
                    .rotationEffect(angleForPosition(position))
                    .offset(offsetForPosition(position))
                toolTipView
            } else {
                toolTipView
                Triangle()
                    .fill(Color.white)
                    .frame(width: 20, height: 20)
                    .rotationEffect(angleForPosition(position))
                    .offset(offsetForPosition(position))
            }
        }
    }
    
    @ViewBuilder
    var toolTipView: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text(title)
            Text(subtitle)
            HStack {
                Button(action: primaryAction) {
                    HStack {
                        Text(primaryButtonTitle)
                            .padding()
                            .cornerRadius(4)
                        Spacer()
                    }
                }
                
                Button(action: secondaryAction) {
                    Text(secondaryButtonTitle)
                        .padding()
                        .frame(minWidth: 116)
                        .cornerRadius(4)
                }
            }
        }
        .padding(12)
        .background(Color.white)
        .cornerRadius(4)
        .padding(20)
    }
    
    func angleForPosition(_ position: ToolTipPosition) -> Angle {
        switch position {
        case .top: return .degrees(180)
        case .bottom: return .degrees(0)
        }
    }
    
    func offsetForPosition(_ position: ToolTipPosition) -> CGSize {
        switch position {
        case .top: return CGSize(width: 0, height: 20)
        case .bottom: return CGSize(width: 0, height: -20)
        }
    }
}

struct ToolTipModifier : ViewModifier {
    
    let customToolTipView: AnyView
    let x: CGFloat
    let y: CGFloat
    let primaryAction: () -> Void
    let secondaryAction: () -> Void
    
    @State private var isShowingToolTip = false
    
    func body(content: Content) -> some View {
        ZStack {
            content
                .onTapGesture {
                    isShowingToolTip.toggle()
                }
            if isShowingToolTip {
                ZStack {
                    Color.black.opacity(0.5)
                        .edgesIgnoringSafeArea(.all)
                        .onTapGesture {
                            isShowingToolTip.toggle()
                        }
                    customToolTipView
                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
                        .offset(x: x, y: y)
                        .zIndex(2)
                }
                .edgesIgnoringSafeArea(.all)
            }
        }
    }
}

extension View {
    func toolTip(customToolTipView: AnyView, x: CGFloat, y: CGFloat, primaryAction: @escaping () -> Void, secondaryAction: @escaping () -> Void) -> some View {
        self.modifier(ToolTipModifier(customToolTipView: customToolTipView, x: x, y: y, primaryAction: primaryAction, secondaryAction: secondaryAction))
    }
}

struct Triangle: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.minX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
        return path
    }
}
0

There are 0 answers