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.
Below is what I've been able to achieve.
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
}
}


